From 76066e0a89ec48ae922a86376c42208628dc3197 Mon Sep 17 00:00:00 2001 From: Andreas Schulz Date: Fri, 30 Aug 2024 16:30:33 +0200 Subject: [PATCH 1/3] iOS Sheet and Dialog Imporvements --- .../calf/ui/dialog/AdaptiveAlertDialog.ios.kt | 10 ++- .../calf/ui/dialog/AlertDialogManager.kt | 5 +- .../calf/ui/sheet/AdaptiveBottomSheet.ios.kt | 1 + .../calf/ui/sheet/BottomSheetManager.kt | 13 ++- .../calf/ui/sheet/SheetState.ios.kt | 2 +- .../screens/BottomSheetScreen.kt | 80 +++++++++++++++++-- 6 files changed, 96 insertions(+), 15 deletions(-) diff --git a/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/dialog/AdaptiveAlertDialog.ios.kt b/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/dialog/AdaptiveAlertDialog.ios.kt index 0378dea..16d88ff 100644 --- a/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/dialog/AdaptiveAlertDialog.ios.kt +++ b/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/dialog/AdaptiveAlertDialog.ios.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import androidx.compose.ui.interop.LocalUIViewController import androidx.compose.ui.window.DialogProperties import com.mohamedrejeb.calf.core.InternalCalfApi @@ -18,8 +19,11 @@ actual fun AdaptiveAlertDialog( text: String, properties: DialogProperties ) { - val alertDialogManager = remember { + val currentUIViewController = LocalUIViewController.current + + val alertDialogManager = remember(currentUIViewController) { AlertDialogManager( + parentUIViewController = currentUIViewController, onConfirm = onConfirm, onDismiss = onDismiss, confirmText = confirmText, @@ -40,11 +44,9 @@ actual fun AdaptiveAlertDialog( alertDialogManager.properties = properties } - LaunchedEffect(Unit) { + DisposableEffect(Unit) { alertDialogManager.showAlertDialog() - } - DisposableEffect(Unit) { onDispose { alertDialogManager.dismiss() } diff --git a/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/dialog/AlertDialogManager.kt b/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/dialog/AlertDialogManager.kt index 8ad20eb..18ea0c6 100644 --- a/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/dialog/AlertDialogManager.kt +++ b/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/dialog/AlertDialogManager.kt @@ -24,6 +24,7 @@ import platform.objc.sel_registerName */ @InternalCalfApi class AlertDialogManager internal constructor( + private val parentUIViewController: UIViewController, internal var onConfirm: () -> Unit, internal var onDismiss: () -> Unit, internal var confirmText: String, @@ -47,7 +48,7 @@ class AlertDialogManager internal constructor( * Lambda that dismisses the dialog. */ private val onDismissLambda: (() -> Unit) = { - UIApplication.sharedApplication.keyWindow?.rootViewController?.dismissViewControllerAnimated( + parentUIViewController.dismissViewControllerAnimated( flag = true, completion = { isPresented = false @@ -106,7 +107,7 @@ class AlertDialogManager internal constructor( ) alertController.addAction(cancelAction) - UIApplication.sharedApplication.keyWindow?.rootViewController?.presentViewController( + parentUIViewController.presentViewController( viewControllerToPresent = alertController, animated = true, completion = { diff --git a/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/sheet/AdaptiveBottomSheet.ios.kt b/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/sheet/AdaptiveBottomSheet.ios.kt index 011512c..7603535 100644 --- a/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/sheet/AdaptiveBottomSheet.ios.kt +++ b/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/sheet/AdaptiveBottomSheet.ios.kt @@ -71,6 +71,7 @@ actual fun AdaptiveBottomSheet( onDismiss = { onDismissRequest() }, + confirmValueChange = adaptiveSheetState.confirmValueChange, content = { val sheetCompositionLocalContext = currentCompositionLocalContext diff --git a/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/sheet/BottomSheetManager.kt b/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/sheet/BottomSheetManager.kt index ebd6da4..c216981 100644 --- a/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/sheet/BottomSheetManager.kt +++ b/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/sheet/BottomSheetManager.kt @@ -1,5 +1,7 @@ package com.mohamedrejeb.calf.ui.sheet +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetValue import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.window.ComposeUIViewController @@ -21,11 +23,13 @@ import platform.darwin.NSObject * @param onDismiss The callback that is called when the bottom sheet is dismissed. * @param content The Compose content of the bottom sheet. */ +@OptIn(ExperimentalMaterial3Api::class) internal class BottomSheetManager( private val parentUIViewController: UIViewController, private var isDark: Boolean, private var containerColor: Color, private var onDismiss: () -> Unit, + private val confirmValueChange: (SheetValue) -> Boolean, private val content: @Composable () -> Unit ) { private var isInitialized = false @@ -48,7 +52,8 @@ internal class BottomSheetManager( onDismiss = { isPresented = false onDismiss() - } + }, + confirmValueChange = confirmValueChange, ) } @@ -133,12 +138,14 @@ internal class BottomSheetManager( } } +@OptIn(ExperimentalMaterial3Api::class) class BottomSheetControllerDelegate( - private val onDismiss: () -> Unit + private val onDismiss: () -> Unit, + private val confirmValueChange: (SheetValue) -> Boolean, ) : NSObject(), UIAdaptivePresentationControllerDelegateProtocol { override fun presentationControllerShouldDismiss(presentationController: UIPresentationController): Boolean { - return true + return confirmValueChange(SheetValue.Hidden) } override fun presentationControllerDidDismiss(presentationController: UIPresentationController) { diff --git a/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/sheet/SheetState.ios.kt b/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/sheet/SheetState.ios.kt index 471e943..b3b6449 100644 --- a/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/sheet/SheetState.ios.kt +++ b/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/sheet/SheetState.ios.kt @@ -19,7 +19,7 @@ actual constructor( internal val skipPartiallyExpanded: Boolean, density: Density, initialValue: SheetValue, - confirmValueChange: (SheetValue) -> Boolean, + internal val confirmValueChange: (SheetValue) -> Boolean, skipHiddenState: Boolean, ) { init { diff --git a/sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/screens/BottomSheetScreen.kt b/sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/screens/BottomSheetScreen.kt index a0f8a23..44e06a5 100644 --- a/sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/screens/BottomSheetScreen.kt +++ b/sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/screens/BottomSheetScreen.kt @@ -1,18 +1,42 @@ package com.mohamedrejeb.calf.sample.screens import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.mohamedrejeb.calf.ui.dialog.AdaptiveAlertDialog import com.mohamedrejeb.calf.ui.sheet.AdaptiveBottomSheet import com.mohamedrejeb.calf.ui.sheet.rememberAdaptiveSheetState +import com.mohamedrejeb.calf.ui.toggle.AdaptiveSwitch import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -20,8 +44,13 @@ import kotlinx.coroutines.launch fun BottomSheetScreen( navigateBack: () -> Unit ) { + var allowHide by remember { mutableStateOf(true) } val scope = rememberCoroutineScope() - val sheetState = rememberAdaptiveSheetState() + val sheetState = rememberAdaptiveSheetState( + confirmValueChange = { + it != SheetValue.Hidden || allowHide + } + ) var openBottomSheet by rememberSaveable { mutableStateOf(false) } Box( @@ -62,6 +91,8 @@ fun BottomSheetScreen( }, adaptiveSheetState = sheetState, ) { + var showDialog by remember { mutableStateOf(false) } + LazyColumn( modifier = Modifier .background(color = MaterialTheme.colorScheme.surface) @@ -82,6 +113,45 @@ fun BottomSheetScreen( } } + item { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + AdaptiveSwitch( + checked = allowHide, + onCheckedChange = { + allowHide = it + } + ) + Text("Allow swipe to hide") + } + } + + item { + Button( + onClick = { + showDialog = true + }, + ) { + Text("Show Alert Dialog") + } + + if (showDialog) { + AdaptiveAlertDialog( + onConfirm = { + showDialog = false + }, + onDismiss = { + showDialog = false + }, + confirmText = "Ok", + dismissText = "Cancel", + title = "Alert Dialog", + text = "This is a native alert dialog from Calf", + ) + } + } + items(100) { Text( text = "Item $it", From ff122501540f63250c7198da46532ee8a2fde226 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sat, 31 Aug 2024 14:07:57 +0200 Subject: [PATCH 2/3] Fix Dialog Dismiss --- .../calf/ui/dialog/AlertDialogManager.kt | 63 ++++++++++--------- sample/ios/Calf.xcodeproj/project.pbxproj | 4 +- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/dialog/AlertDialogManager.kt b/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/dialog/AlertDialogManager.kt index 18ea0c6..af000b4 100644 --- a/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/dialog/AlertDialogManager.kt +++ b/calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/dialog/AlertDialogManager.kt @@ -18,7 +18,7 @@ import platform.objc.sel_registerName * @param title The title of the dialog. * @param text The text of the dialog. * @param properties The properties of the dialog. - * @param uiViewController The [UIViewController] that will present the dialog. + * @param parentUIViewController The [UIViewController] that will present the dialog. * @constructor Creates an [AlertDialogManager] instance. * @see [UIAlertController] */ @@ -44,11 +44,40 @@ class AlertDialogManager internal constructor( */ private var isAnimating = false + /** + * The ui view controller that is used to present the dialog. + */ + private val dialogUIViewController: UIViewController by lazy { + UIAlertController.alertControllerWithTitle( + title = title, + message = text, + preferredStyle = UIAlertControllerStyleAlert + ).apply { + val confirmAction = UIAlertAction.actionWithTitle( + title = confirmText, + style = UIAlertActionStyleDefault, + handler = { + onConfirm() + } + ) + addAction(confirmAction) + + val cancelAction = UIAlertAction.actionWithTitle( + title = dismissText, + style = UIAlertActionStyleDestructive, + handler = { + onDismiss() + } + ) + addAction(cancelAction) + } + } + /** * Lambda that dismisses the dialog. */ private val onDismissLambda: (() -> Unit) = { - parentUIViewController.dismissViewControllerAnimated( + dialogUIViewController.dismissViewControllerAnimated( flag = true, completion = { isPresented = false @@ -83,40 +112,16 @@ class AlertDialogManager internal constructor( if (isPresented || isAnimating) return isAnimating = true - val alertController = UIAlertController.alertControllerWithTitle( - title = title, - message = text, - preferredStyle = UIAlertControllerStyleAlert - ) - - val confirmAction = UIAlertAction.actionWithTitle( - title = confirmText, - style = UIAlertActionStyleDefault, - handler = { - onConfirm() - } - ) - alertController.addAction(confirmAction) - - val cancelAction = UIAlertAction.actionWithTitle( - title = dismissText, - style = UIAlertActionStyleDestructive, - handler = { - onDismiss() - } - ) - alertController.addAction(cancelAction) - parentUIViewController.presentViewController( - viewControllerToPresent = alertController, + viewControllerToPresent = dialogUIViewController, animated = true, completion = { isPresented = true isAnimating = false if (properties.dismissOnClickOutside) { - alertController.view.superview?.setUserInteractionEnabled(true) - alertController.view.superview?.addGestureRecognizer( + dialogUIViewController.view.superview?.setUserInteractionEnabled(true) + dialogUIViewController.view.superview?.addGestureRecognizer( UITapGestureRecognizer( target = this, action = dismissPointer diff --git a/sample/ios/Calf.xcodeproj/project.pbxproj b/sample/ios/Calf.xcodeproj/project.pbxproj index 6f2dae9..ed62ce8 100644 --- a/sample/ios/Calf.xcodeproj/project.pbxproj +++ b/sample/ios/Calf.xcodeproj/project.pbxproj @@ -291,7 +291,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Calf/Preview Content\""; - DEVELOPMENT_TEAM = V7T9SX3F3Z; + DEVELOPMENT_TEAM = 2GHSF2M7P7; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../common/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; GENERATE_INFOPLIST_FILE = YES; @@ -328,7 +328,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Calf/Preview Content\""; - DEVELOPMENT_TEAM = V7T9SX3F3Z; + DEVELOPMENT_TEAM = 2GHSF2M7P7; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../common/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; GENERATE_INFOPLIST_FILE = YES; From 28f58c8237b4ccd17bc124a589f3f8092f3fd058 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sat, 31 Aug 2024 16:20:25 +0200 Subject: [PATCH 3/3] Revert TeamID --- sample/ios/Calf.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sample/ios/Calf.xcodeproj/project.pbxproj b/sample/ios/Calf.xcodeproj/project.pbxproj index ed62ce8..6f2dae9 100644 --- a/sample/ios/Calf.xcodeproj/project.pbxproj +++ b/sample/ios/Calf.xcodeproj/project.pbxproj @@ -291,7 +291,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Calf/Preview Content\""; - DEVELOPMENT_TEAM = 2GHSF2M7P7; + DEVELOPMENT_TEAM = V7T9SX3F3Z; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../common/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; GENERATE_INFOPLIST_FILE = YES; @@ -328,7 +328,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Calf/Preview Content\""; - DEVELOPMENT_TEAM = 2GHSF2M7P7; + DEVELOPMENT_TEAM = V7T9SX3F3Z; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../common/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; GENERATE_INFOPLIST_FILE = YES;