From 91d4c7467c402c4c85e82ff66c55fee565dde3bc Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Tue, 18 Jun 2024 16:55:50 +0200 Subject: [PATCH 01/18] Refactor UIKit interop --- .../ui/interop/UIKitInteropContainer.uikit.kt | 3 +- .../compose/ui/interop/UIKitView.uikit.kt | 326 ++++++++++-------- 2 files changed, 176 insertions(+), 153 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt index 985d818fabc86..c18fa568e6879 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt @@ -78,7 +78,8 @@ private class UIKitInteropContainerView: UIView(CGRectZero.readValue()) { } /** - * Modifier to track interop view inside [LayoutNode] hierarchy. + * Modifier to track interop view inside [LayoutNode] hierarchy. Used to properly + * sort interop views in the tree. * * @param view The [UIView] that matches the current node. */ diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt index a178a8d30233c..0eed108965e2a 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateObserver import androidx.compose.ui.Modifier -import androidx.compose.ui.* +import androidx.compose.runtime.State import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color @@ -47,6 +47,7 @@ import androidx.compose.ui.unit.round import androidx.compose.ui.unit.toDpRect import androidx.compose.ui.unit.toRect import androidx.compose.ui.unit.width +import androidx.compose.ui.node.TrackInteropModifierNode import kotlinx.atomicfu.atomic import kotlinx.cinterop.CValue import platform.CoreGraphics.CGRect @@ -64,9 +65,10 @@ import platform.CoreGraphics.CGRectZero private val STUB_CALLBACK_WITH_RECEIVER: Any.() -> Unit = {} private val DefaultViewResize: UIView.(CValue) -> Unit = { rect -> this.setFrame(rect) } -private val DefaultViewControllerResize: UIViewController.(CValue) -> Unit = { rect -> this.view.setFrame(rect) } +private val DefaultViewControllerResize: UIViewController.(CValue) -> Unit = + { rect -> this.view.setFrame(rect) } -internal class InteropWrappingView: CMPInteropWrappingView(frame = CGRectZero.readValue()) { +internal class InteropWrappingView : CMPInteropWrappingView(frame = CGRectZero.readValue()) { var actualAccessibilityContainer: Any? = null override fun accessibilityContainer(): Any? { @@ -80,10 +82,12 @@ internal val InteropViewSemanticsKey = AccessibilityKey( if (parentValue == null) { childValue } else { - println("Warning: Merging accessibility for multiple interop views is not supported. " + - "Multiple [UIKitView] are grouped under one node that should be represented as a single accessibility element." + - "It isn't recommended because the accessibility system can only recognize the first one. " + - "If you need multiple native views for accessibility, make sure to place them inside a single [UIKitView].") + println( + "Warning: Merging accessibility for multiple interop views is not supported. " + + "Multiple [UIKitView] are grouped under one node that should be represented as a single accessibility element." + + "It isn't recommended because the accessibility system can only recognize the first one. " + + "If you need multiple native views for accessibility, make sure to place them inside a single [UIKitView]." + ) parentValue } @@ -96,7 +100,10 @@ private var SemanticsPropertyReceiver.interopView by InteropViewSemanticsKey * Chain [this] with [Modifier.semantics] that sets the [interopView] of the node if [enabled] is true. * If [enabled] is false, [this] is returned as is. */ -private fun Modifier.interopSemantics(enabled: Boolean, wrappingView: InteropWrappingView): Modifier = +private fun Modifier.interopSemantics( + enabled: Boolean, + wrappingView: InteropWrappingView +): Modifier = if (enabled) { this.semantics { interopView = wrappingView @@ -105,58 +112,31 @@ private fun Modifier.interopSemantics(enabled: Boolean, wrappingView: InteropWra this } +private fun Modifier.catchInteropPointer(isInteractive: Boolean): Modifier = + if (isInteractive) { + this then InteropViewCatchPointerModifier() + } else { + this + } + /** - * @param factory The block creating the [UIView] to be composed. - * @param modifier The modifier to be applied to the layout. Size should be specified in modifier. - * Modifier may contains crop() modifier with different shapes. - * @param update A callback to be invoked after the layout is inflated. - * @param background A color of [UIView] background wrapping the view created by [factory]. - * @param onRelease A callback invoked as a signal that this view instance has exited the - * composition hierarchy entirely and will not be reused again. Any additional resources used by the - * View should be freed at this time. - * @param onResize May be used to custom resize logic. - * @param interactive If true, then user touches will be passed to this UIView - * @param accessibilityEnabled If `true`, then the view will be visible to accessibility services. - * - * If this Composable is within a modifier chain that merges - * the semantics of its children (such as `Modifier.clickable`), the merged subtree data will be ignored in favor of - * the native UIAccessibility resolution for the view constructed by [factory]. For example, `Button` containing [UIKitView] - * will be invisible for accessibility services, only the [UIView] created by [factory] will be accessible. - * To avoid this behavior, set [accessibilityEnabled] to `false` and use custom [Modifier.semantics] for `Button` to - * make the information associated with this view accessible. - * - * If there are multiple [UIKitView] or [UIKitViewController] with [accessibilityEnabled] set to `true` in the merged tree, only the first one will be accessible. - * Consider using a single [UIKitView] or [UIKitViewController] with multiple views inside it if you need multiple accessible views. - * - * In general, [accessibilityEnabled] set to `true` is not recommended to use in such cases. - * Consider using [Modifier.semantics] on Composable that merges its semantics instead. - * - * @see Modifier.semantics + * Internal common part of custom layout emitting a node associated with UIKit interop for [UIView] and [UIViewController]. */ @Composable -fun UIKitView( - factory: () -> T, +private fun UIKitInteropLayout( modifier: Modifier, - update: (T) -> Unit = STUB_CALLBACK_WITH_RECEIVER, - background: Color = Color.Unspecified, - onRelease: (T) -> Unit = STUB_CALLBACK_WITH_RECEIVER, - onResize: (view: T, rect: CValue) -> Unit = DefaultViewResize, - interactive: Boolean = true, - accessibilityEnabled: Boolean = true, + update: (T) -> Unit, + background: Color, + entityHandler: InteropEntityHandler, + onResize: (T, rect: CValue) -> Unit, + interactive: Boolean, + accessibilityEnabled: Boolean, ) { - // TODO: adapt UIKitView to reuse inside LazyColumn like in AndroidView: - // https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1) - val interopContainer = LocalUIKitInteropContainer.current - val embeddedInteropComponent = remember { - EmbeddedInteropView( - interopContainer = interopContainer, - onRelease - ) - } val density = LocalDensity.current var rectInPixels by remember { mutableStateOf(IntRect(0, 0, 0, 0)) } var localToWindowOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } val interopContext = LocalUIKitInteropContext.current + val interopContainer = LocalUIKitInteropContainer.current EmptyLayout( modifier.onGloballyPositioned { coordinates -> @@ -166,14 +146,19 @@ fun UIKitView( val rect = newRectInPixels.toRect().toDpRect(density) interopContext.deferAction { - embeddedInteropComponent.wrappingView.setFrame(rect.asCGRect()) + entityHandler.wrappingView.setFrame(rect.asCGRect()) } if (rectInPixels.width != newRectInPixels.width || rectInPixels.height != newRectInPixels.height) { interopContext.deferAction { onResize( - embeddedInteropComponent.component, - CGRectMake(0.0, 0.0, rect.width.value.toDouble(), rect.height.value.toDouble()), + entityHandler.interopEntity, + CGRectMake( + 0.0, + 0.0, + rect.width.value.toDouble(), + rect.height.value.toDouble() + ), ) } } @@ -181,42 +166,102 @@ fun UIKitView( } }.drawBehind { // Clear interop area to make visible the component under our canvas. - drawRect(Color.Transparent, blendMode = BlendMode.Clear) - }.trackUIKitInterop(interopContainer, embeddedInteropComponent.wrappingView).let { - if (interactive) { - it.then(InteropViewCatchPointerModifier()) - } else { - it - } - }.interopSemantics(accessibilityEnabled, embeddedInteropComponent.wrappingView) + drawRect( + color = Color.Transparent, + blendMode = BlendMode.Clear + ) + }.trackUIKitInterop( + container = interopContainer, + view = entityHandler.wrappingView + ).catchInteropPointer( + isInteractive = interactive + ).interopSemantics(accessibilityEnabled, entityHandler.wrappingView) ) DisposableEffect(Unit) { - embeddedInteropComponent.component = factory() - embeddedInteropComponent.updater = Updater(embeddedInteropComponent.component, update) { - interopContext.deferAction(action = it) - } + entityHandler.initialize(interopContext, update) interopContext.deferAction(UIKitInteropViewHierarchyChange.VIEW_ADDED) { - embeddedInteropComponent.addToHierarchy() + entityHandler.addToHierarchy() } onDispose { interopContext.deferAction(UIKitInteropViewHierarchyChange.VIEW_REMOVED) { - embeddedInteropComponent.removeFromHierarchy() + entityHandler.removeFromHierarchy() } } } LaunchedEffect(background) { interopContext.deferAction { - embeddedInteropComponent.setBackgroundColor(background) + entityHandler.setBackgroundColor(background) } } SideEffect { - embeddedInteropComponent.updater.update = update + entityHandler.update = update + } +} + +/** + * @param factory The block creating the [UIView] to be composed. + * @param modifier The modifier to be applied to the layout. Size should be specified in modifier. + * Modifier may contains crop() modifier with different shapes. + * @param update A callback to be invoked after the layout is inflated. + * @param background A color of [UIView] background wrapping the view created by [factory]. + * @param onRelease A callback invoked as a signal that this view instance has exited the + * composition hierarchy entirely and will not be reused again. Any additional resources used by the + * View should be freed at this time. + * @param onResize May be used to custom resize logic. + * @param interactive If true, then user touches will be passed to this UIView + * @param accessibilityEnabled If `true`, then the view will be visible to accessibility services. + * + * If this Composable is within a modifier chain that merges + * the semantics of its children (such as `Modifier.clickable`), the merged subtree data will be ignored in favor of + * the native UIAccessibility resolution for the view constructed by [factory]. For example, `Button` containing [UIKitView] + * will be invisible for accessibility services, only the [UIView] created by [factory] will be accessible. + * To avoid this behavior, set [accessibilityEnabled] to `false` and use custom [Modifier.semantics] for `Button` to + * make the information associated with this view accessible. + * + * If there are multiple [UIKitView] or [UIKitViewController] with [accessibilityEnabled] set to `true` in the merged tree, only the first one will be accessible. + * Consider using a single [UIKitView] or [UIKitViewController] with multiple views inside it if you need multiple accessible views. + * + * In general, [accessibilityEnabled] set to `true` is not recommended to use in such cases. + * Consider using [Modifier.semantics] on Composable that merges its semantics instead. + * + * @see Modifier.semantics + */ +@Composable +fun UIKitView( + factory: () -> T, + modifier: Modifier, + update: (T) -> Unit = STUB_CALLBACK_WITH_RECEIVER, + background: Color = Color.Unspecified, + onRelease: (T) -> Unit = STUB_CALLBACK_WITH_RECEIVER, + onResize: (view: T, rect: CValue) -> Unit = DefaultViewResize, + interactive: Boolean = true, + accessibilityEnabled: Boolean = true, +) { + // TODO: adapt UIKitView to reuse inside LazyColumn like in AndroidView: + // https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1) + val interopContainer = LocalUIKitInteropContainer.current + val handler = remember { + InteropViewHandler( + makeInteropEntity = factory, + interopContainer = interopContainer, + onRelease = onRelease + ) } + + UIKitInteropLayout( + modifier = modifier, + update = update, + background = background, + entityHandler = handler, + onResize = onResize, + interactive = interactive, + accessibilityEnabled = accessibilityEnabled + ) } /** @@ -264,87 +309,50 @@ fun UIKitViewController( // https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1) val interopContainer = LocalUIKitInteropContainer.current val rootViewController = LocalUIViewController.current - val embeddedInteropComponent = remember { - EmbeddedInteropViewController( + val handler = remember { + InteropViewControllerHandler( + makeInteropEntity = factory, interopContainer = interopContainer, rootViewController = rootViewController, onRelease = onRelease ) } - val density = LocalDensity.current - var rectInPixels by remember { mutableStateOf(IntRect(0, 0, 0, 0)) } - var localToWindowOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } - val interopContext = LocalUIKitInteropContext.current - - EmptyLayout( - modifier.onGloballyPositioned { coordinates -> - localToWindowOffset = coordinates.positionInRoot().round() - val newRectInPixels = IntRect(localToWindowOffset, coordinates.size) - if (rectInPixels != newRectInPixels) { - val rect = newRectInPixels.toRect().toDpRect(density) - - interopContext.deferAction { - embeddedInteropComponent.wrappingView.setFrame(rect.asCGRect()) - } - - if (rectInPixels.width != newRectInPixels.width || rectInPixels.height != newRectInPixels.height) { - interopContext.deferAction { - onResize( - embeddedInteropComponent.component, - CGRectMake(0.0, 0.0, rect.width.value.toDouble(), rect.height.value.toDouble()), - ) - } - } - rectInPixels = newRectInPixels - } - }.drawBehind { - // Clear interop area to make visible the component under our canvas. - drawRect(Color.Transparent, blendMode = BlendMode.Clear) - }.trackUIKitInterop(interopContainer, embeddedInteropComponent.wrappingView).let { - if (interactive) { - it.then(InteropViewCatchPointerModifier()) - } else { - it - } - }.interopSemantics(accessibilityEnabled, embeddedInteropComponent.wrappingView) + UIKitInteropLayout( + modifier = modifier, + update = update, + background = background, + entityHandler = handler, + onResize = onResize, + interactive = interactive, + accessibilityEnabled = accessibilityEnabled ) - - DisposableEffect(Unit) { - embeddedInteropComponent.component = factory() - embeddedInteropComponent.updater = Updater(embeddedInteropComponent.component, update) { - interopContext.deferAction(action = it) - } - - interopContext.deferAction(UIKitInteropViewHierarchyChange.VIEW_ADDED) { - embeddedInteropComponent.addToHierarchy() - } - - onDispose { - interopContext.deferAction(UIKitInteropViewHierarchyChange.VIEW_REMOVED) { - embeddedInteropComponent.removeFromHierarchy() - } - } - } - - LaunchedEffect(background) { - interopContext.deferAction { - embeddedInteropComponent.setBackgroundColor(background) - } - } - - SideEffect { - embeddedInteropComponent.updater.update = update - } } -private abstract class EmbeddedInteropComponent( +/** + * An abstract that abstracts structural and state management of interop entities like [UIView] and [UIViewController] + */ +private abstract class InteropEntityHandler( + val makeInteropEntity: () -> T, val interopContainer: UIKitInteropContainer, val onRelease: (T) -> Unit ) { val wrappingView = InteropWrappingView() - lateinit var component: T - lateinit var updater: Updater + lateinit var interopEntity: T + private lateinit var updater: Updater + + var update: (T) -> Unit + get() = updater.update + set(value) { + updater.update = value + } + + fun initialize(interopContext: UIKitInteropContext, update: (T) -> Unit) { + interopEntity = makeInteropEntity() + updater = Updater(interopEntity, update) { + interopContext.deferAction(action = it) + } + } fun setBackgroundColor(color: Color) { if (color == Color.Unspecified) { @@ -357,50 +365,64 @@ private abstract class EmbeddedInteropComponent( abstract fun addToHierarchy() abstract fun removeFromHierarchy() + /** + * Places the actual view a user constructed in factory to the [wrappingView] + * [wrappingView] will be added to the [UIKitInteropContainer] within [TrackInteropModifierNode] + */ protected fun addViewToHierarchy(view: UIView) { wrappingView.addSubview(view) - // wrappingView will be added to the container from [onPlaced] } protected fun removeViewFromHierarchy(view: UIView) { view.removeFromSuperview() interopContainer.removeInteropView(wrappingView) updater.dispose() - onRelease(component) + onRelease(interopEntity) } } -private class EmbeddedInteropView( +private class InteropViewHandler( + makeInteropEntity: () -> T, interopContainer: UIKitInteropContainer, onRelease: (T) -> Unit -) : EmbeddedInteropComponent(interopContainer, onRelease) { +) : InteropEntityHandler(makeInteropEntity, interopContainer, onRelease) { override fun addToHierarchy() { - addViewToHierarchy(component) + addViewToHierarchy(interopEntity) } override fun removeFromHierarchy() { - removeViewFromHierarchy(component) + removeViewFromHierarchy(interopEntity) } } -private class EmbeddedInteropViewController( +private class InteropViewControllerHandler( + makeInteropEntity: () -> T, interopContainer: UIKitInteropContainer, private val rootViewController: UIViewController, onRelease: (T) -> Unit -) : EmbeddedInteropComponent(interopContainer, onRelease) { +) : InteropEntityHandler(makeInteropEntity, interopContainer, onRelease) { override fun addToHierarchy() { - rootViewController.addChildViewController(component) - addViewToHierarchy(component.view) - component.didMoveToParentViewController(rootViewController) + rootViewController.addChildViewController(interopEntity) + addViewToHierarchy(interopEntity.view) + interopEntity.didMoveToParentViewController(rootViewController) } override fun removeFromHierarchy() { - component.willMoveToParentViewController(null) - removeViewFromHierarchy(component.view) - component.removeFromParentViewController() + interopEntity.willMoveToParentViewController(null) + removeViewFromHierarchy(interopEntity.view) + interopEntity.removeFromParentViewController() } } +/** + * A helper class to schedule an update the interop entity whenever the [State] used by the [update] + * lambda is changed. + * + * @param component The interop entity to be updated. + * @param update The lambda to be called whenever the state used by this lambda is changed. + * @param deferAction The lambda to register [update] execution to defer it in order to sync it with + * Compose rendering. The aim of this is to make changes to UIKit and Compose synchronized. + */ private class Updater( private val component: T, update: (T) -> Unit, From de0790b7533ff15522e36d3ecd32c5e88f4a8cc2 Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Tue, 18 Jun 2024 17:02:22 +0200 Subject: [PATCH 02/18] Refactor UIKit interop --- .../compose/ui/interop/UIKitView.uikit.kt | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt index 0eed108965e2a..3b6d8555e5fb9 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt @@ -138,8 +138,8 @@ private fun UIKitInteropLayout( val interopContext = LocalUIKitInteropContext.current val interopContainer = LocalUIKitInteropContainer.current - EmptyLayout( - modifier.onGloballyPositioned { coordinates -> + val finalModifier = modifier + .onGloballyPositioned { coordinates -> localToWindowOffset = coordinates.positionInRoot().round() val newRectInPixels = IntRect(localToWindowOffset, coordinates.size) if (rectInPixels != newRectInPixels) { @@ -164,18 +164,20 @@ private fun UIKitInteropLayout( } rectInPixels = newRectInPixels } - }.drawBehind { + } + .drawBehind { // Clear interop area to make visible the component under our canvas. drawRect( color = Color.Transparent, blendMode = BlendMode.Clear ) - }.trackUIKitInterop( - container = interopContainer, - view = entityHandler.wrappingView - ).catchInteropPointer( - isInteractive = interactive - ).interopSemantics(accessibilityEnabled, entityHandler.wrappingView) + } + .trackUIKitInterop(interopContainer, entityHandler.wrappingView) + .catchInteropPointer(interactive) + .interopSemantics(accessibilityEnabled, entityHandler.wrappingView) + + EmptyLayout( + finalModifier ) DisposableEffect(Unit) { From 8cfe1e747821e65eb1432b6b1faee8a519a78575 Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Tue, 18 Jun 2024 17:21:21 +0200 Subject: [PATCH 03/18] Update comment --- .../androidx/compose/ui/interop/UIKitView.uikit.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt index 3b6d8555e5fb9..d75dae2bb080b 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt @@ -154,10 +154,10 @@ private fun UIKitInteropLayout( onResize( entityHandler.interopEntity, CGRectMake( - 0.0, - 0.0, - rect.width.value.toDouble(), - rect.height.value.toDouble() + x = 0.0, + y = 0.0, + width = rect.width.value.toDouble(), + height = rect.height.value.toDouble() ), ) } @@ -332,7 +332,7 @@ fun UIKitViewController( } /** - * An abstract that abstracts structural and state management of interop entities like [UIView] and [UIViewController] + * An abstract class responsible for hiearchy updates and state management of interop entities like [UIView] and [UIViewController] */ private abstract class InteropEntityHandler( val makeInteropEntity: () -> T, From d39c221de235df50b99008e1b3989fb5f8980300 Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Tue, 18 Jun 2024 17:22:38 +0200 Subject: [PATCH 04/18] Move todo --- .../kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt index d75dae2bb080b..ad93dcd2a056b 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt @@ -244,8 +244,6 @@ fun UIKitView( interactive: Boolean = true, accessibilityEnabled: Boolean = true, ) { - // TODO: adapt UIKitView to reuse inside LazyColumn like in AndroidView: - // https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1) val interopContainer = LocalUIKitInteropContainer.current val handler = remember { InteropViewHandler( @@ -307,8 +305,6 @@ fun UIKitViewController( interactive: Boolean = true, accessibilityEnabled: Boolean = true, ) { - // TODO: adapt UIKitViewController to reuse inside LazyColumn like in AndroidView: - // https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1) val interopContainer = LocalUIKitInteropContainer.current val rootViewController = LocalUIViewController.current val handler = remember { @@ -335,6 +331,8 @@ fun UIKitViewController( * An abstract class responsible for hiearchy updates and state management of interop entities like [UIView] and [UIViewController] */ private abstract class InteropEntityHandler( + // TODO: reuse an object created makeInteropEntity inside LazyColumn like in AndroidView: + // https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1) val makeInteropEntity: () -> T, val interopContainer: UIKitInteropContainer, val onRelease: (T) -> Unit From 7e0ca166de1a77e40caee74ec6def0509c6a077a Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Wed, 19 Jun 2024 11:44:57 +0200 Subject: [PATCH 05/18] Update doc --- .../kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt index ad93dcd2a056b..bcc88a953377c 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt @@ -415,13 +415,15 @@ private class InteropViewControllerHandler( } /** - * A helper class to schedule an update the interop entity whenever the [State] used by the [update] + * A helper class to schedule an update for the interop entity whenever the [State] used by the [update] * lambda is changed. * * @param component The interop entity to be updated. * @param update The lambda to be called whenever the state used by this lambda is changed. * @param deferAction The lambda to register [update] execution to defer it in order to sync it with - * Compose rendering. The aim of this is to make changes to UIKit and Compose synchronized. + * Compose rendering. The aim of this is to make visual changes to UIKit and Compose + * simulteneously. + * @see [UIKitInteropContext] and [UIKitInteropTransaction] for more details. */ private class Updater( private val component: T, From f9c7af4e0b3aa38d38b3ba49f7e44c46308b44e4 Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Wed, 19 Jun 2024 11:55:12 +0200 Subject: [PATCH 06/18] Rename entity to component --- .../compose/ui/interop/UIKitView.uikit.kt | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt index bcc88a953377c..6e0460bb57473 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt @@ -127,7 +127,7 @@ private fun UIKitInteropLayout( modifier: Modifier, update: (T) -> Unit, background: Color, - entityHandler: InteropEntityHandler, + componentHandler: InteropComponentHandler, onResize: (T, rect: CValue) -> Unit, interactive: Boolean, accessibilityEnabled: Boolean, @@ -146,13 +146,13 @@ private fun UIKitInteropLayout( val rect = newRectInPixels.toRect().toDpRect(density) interopContext.deferAction { - entityHandler.wrappingView.setFrame(rect.asCGRect()) + componentHandler.wrappingView.setFrame(rect.asCGRect()) } if (rectInPixels.width != newRectInPixels.width || rectInPixels.height != newRectInPixels.height) { interopContext.deferAction { onResize( - entityHandler.interopEntity, + componentHandler.component, CGRectMake( x = 0.0, y = 0.0, @@ -172,36 +172,36 @@ private fun UIKitInteropLayout( blendMode = BlendMode.Clear ) } - .trackUIKitInterop(interopContainer, entityHandler.wrappingView) + .trackUIKitInterop(interopContainer, componentHandler.wrappingView) .catchInteropPointer(interactive) - .interopSemantics(accessibilityEnabled, entityHandler.wrappingView) + .interopSemantics(accessibilityEnabled, componentHandler.wrappingView) EmptyLayout( finalModifier ) DisposableEffect(Unit) { - entityHandler.initialize(interopContext, update) + componentHandler.initialize(interopContext, update) interopContext.deferAction(UIKitInteropViewHierarchyChange.VIEW_ADDED) { - entityHandler.addToHierarchy() + componentHandler.addToHierarchy() } onDispose { interopContext.deferAction(UIKitInteropViewHierarchyChange.VIEW_REMOVED) { - entityHandler.removeFromHierarchy() + componentHandler.removeFromHierarchy() } } } LaunchedEffect(background) { interopContext.deferAction { - entityHandler.setBackgroundColor(background) + componentHandler.setBackgroundColor(background) } } SideEffect { - entityHandler.update = update + componentHandler.update = update } } @@ -247,7 +247,7 @@ fun UIKitView( val interopContainer = LocalUIKitInteropContainer.current val handler = remember { InteropViewHandler( - makeInteropEntity = factory, + makeInteropComponent = factory, interopContainer = interopContainer, onRelease = onRelease ) @@ -257,7 +257,7 @@ fun UIKitView( modifier = modifier, update = update, background = background, - entityHandler = handler, + componentHandler = handler, onResize = onResize, interactive = interactive, accessibilityEnabled = accessibilityEnabled @@ -309,7 +309,7 @@ fun UIKitViewController( val rootViewController = LocalUIViewController.current val handler = remember { InteropViewControllerHandler( - makeInteropEntity = factory, + makeInteropComponent = factory, interopContainer = interopContainer, rootViewController = rootViewController, onRelease = onRelease @@ -320,7 +320,7 @@ fun UIKitViewController( modifier = modifier, update = update, background = background, - entityHandler = handler, + componentHandler = handler, onResize = onResize, interactive = interactive, accessibilityEnabled = accessibilityEnabled @@ -328,17 +328,17 @@ fun UIKitViewController( } /** - * An abstract class responsible for hiearchy updates and state management of interop entities like [UIView] and [UIViewController] + * An abstract class responsible for hiearchy updates and state management of interop components like [UIView] and [UIViewController] */ -private abstract class InteropEntityHandler( - // TODO: reuse an object created makeInteropEntity inside LazyColumn like in AndroidView: +private abstract class InteropComponentHandler( + // TODO: reuse an object created makeInteropComponent inside LazyColumn like in AndroidView: // https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1) - val makeInteropEntity: () -> T, + val makeInteropComponent: () -> T, val interopContainer: UIKitInteropContainer, val onRelease: (T) -> Unit ) { val wrappingView = InteropWrappingView() - lateinit var interopEntity: T + lateinit var component: T private lateinit var updater: Updater var update: (T) -> Unit @@ -348,8 +348,8 @@ private abstract class InteropEntityHandler( } fun initialize(interopContext: UIKitInteropContext, update: (T) -> Unit) { - interopEntity = makeInteropEntity() - updater = Updater(interopEntity, update) { + component = makeInteropComponent() + updater = Updater(component, update) { interopContext.deferAction(action = it) } } @@ -377,52 +377,52 @@ private abstract class InteropEntityHandler( view.removeFromSuperview() interopContainer.removeInteropView(wrappingView) updater.dispose() - onRelease(interopEntity) + onRelease(component) } } private class InteropViewHandler( - makeInteropEntity: () -> T, + makeInteropComponent: () -> T, interopContainer: UIKitInteropContainer, onRelease: (T) -> Unit -) : InteropEntityHandler(makeInteropEntity, interopContainer, onRelease) { +) : InteropComponentHandler(makeInteropComponent, interopContainer, onRelease) { override fun addToHierarchy() { - addViewToHierarchy(interopEntity) + addViewToHierarchy(component) } override fun removeFromHierarchy() { - removeViewFromHierarchy(interopEntity) + removeViewFromHierarchy(component) } } private class InteropViewControllerHandler( - makeInteropEntity: () -> T, + makeInteropComponent: () -> T, interopContainer: UIKitInteropContainer, private val rootViewController: UIViewController, onRelease: (T) -> Unit -) : InteropEntityHandler(makeInteropEntity, interopContainer, onRelease) { +) : InteropComponentHandler(makeInteropComponent, interopContainer, onRelease) { override fun addToHierarchy() { - rootViewController.addChildViewController(interopEntity) - addViewToHierarchy(interopEntity.view) - interopEntity.didMoveToParentViewController(rootViewController) + rootViewController.addChildViewController(component) + addViewToHierarchy(component.view) + component.didMoveToParentViewController(rootViewController) } override fun removeFromHierarchy() { - interopEntity.willMoveToParentViewController(null) - removeViewFromHierarchy(interopEntity.view) - interopEntity.removeFromParentViewController() + component.willMoveToParentViewController(null) + removeViewFromHierarchy(component.view) + component.removeFromParentViewController() } } /** - * A helper class to schedule an update for the interop entity whenever the [State] used by the [update] + * A helper class to schedule an update for the interop component whenever the [State] used by the [update] * lambda is changed. * - * @param component The interop entity to be updated. + * @param component The interop component to be updated. * @param update The lambda to be called whenever the state used by this lambda is changed. * @param deferAction The lambda to register [update] execution to defer it in order to sync it with * Compose rendering. The aim of this is to make visual changes to UIKit and Compose - * simulteneously. + * simultaneously. * @see [UIKitInteropContext] and [UIKitInteropTransaction] for more details. */ private class Updater( From de7a0154612cbccbcd053a5197a3f5d0cb0dc1a2 Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Wed, 19 Jun 2024 12:03:05 +0200 Subject: [PATCH 07/18] Minor renames --- .../compose/ui/interop/UIKitView.uikit.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt index 6e0460bb57473..9d7f734376a9d 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt @@ -247,7 +247,7 @@ fun UIKitView( val interopContainer = LocalUIKitInteropContainer.current val handler = remember { InteropViewHandler( - makeInteropComponent = factory, + createView = factory, interopContainer = interopContainer, onRelease = onRelease ) @@ -309,7 +309,7 @@ fun UIKitViewController( val rootViewController = LocalUIViewController.current val handler = remember { InteropViewControllerHandler( - makeInteropComponent = factory, + createViewController = factory, interopContainer = interopContainer, rootViewController = rootViewController, onRelease = onRelease @@ -331,9 +331,9 @@ fun UIKitViewController( * An abstract class responsible for hiearchy updates and state management of interop components like [UIView] and [UIViewController] */ private abstract class InteropComponentHandler( - // TODO: reuse an object created makeInteropComponent inside LazyColumn like in AndroidView: + // TODO: reuse an object created makeComponent inside LazyColumn like in AndroidView: // https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1) - val makeInteropComponent: () -> T, + val createComponent: () -> T, val interopContainer: UIKitInteropContainer, val onRelease: (T) -> Unit ) { @@ -348,7 +348,7 @@ private abstract class InteropComponentHandler( } fun initialize(interopContext: UIKitInteropContext, update: (T) -> Unit) { - component = makeInteropComponent() + component = createComponent() updater = Updater(component, update) { interopContext.deferAction(action = it) } @@ -382,10 +382,10 @@ private abstract class InteropComponentHandler( } private class InteropViewHandler( - makeInteropComponent: () -> T, + createView: () -> T, interopContainer: UIKitInteropContainer, onRelease: (T) -> Unit -) : InteropComponentHandler(makeInteropComponent, interopContainer, onRelease) { +) : InteropComponentHandler(createView, interopContainer, onRelease) { override fun addToHierarchy() { addViewToHierarchy(component) } @@ -396,11 +396,11 @@ private class InteropViewHandler( } private class InteropViewControllerHandler( - makeInteropComponent: () -> T, + createViewController: () -> T, interopContainer: UIKitInteropContainer, private val rootViewController: UIViewController, onRelease: (T) -> Unit -) : InteropComponentHandler(makeInteropComponent, interopContainer, onRelease) { +) : InteropComponentHandler(createViewController, interopContainer, onRelease) { override fun addToHierarchy() { rootViewController.addChildViewController(component) addViewToHierarchy(component.view) From 52cceb533684892437e011b2b0d5b90d09a4d6f8 Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Wed, 19 Jun 2024 12:10:40 +0200 Subject: [PATCH 08/18] Add docs --- .../kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt index 9d7f734376a9d..0ed9e4883554c 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt @@ -341,6 +341,11 @@ private abstract class InteropComponentHandler( lateinit var component: T private lateinit var updater: Updater + /** + * Access the [update] lambda. + * Lambda is immediately executed when set anew. + * @see Updater.performUpdate + */ var update: (T) -> Unit get() = updater.update set(value) { From d03686739c81b5888a3218705e6a93919d4e16c1 Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Wed, 19 Jun 2024 13:00:12 +0200 Subject: [PATCH 09/18] Merge InteropContext and InteropContainer --- .../ui/interop/LocalUIKitInteropContext.kt | 146 ------------------ .../ui/interop/UIKitInteropContainer.uikit.kt | 133 +++++++++++++++- .../compose/ui/interop/UIKitView.uikit.kt | 91 ++++++----- .../ui/scene/ComposeSceneMediator.uikit.kt | 34 ++-- .../ui/scene/UIViewComposeSceneLayer.uikit.kt | 10 +- .../ui/window/ComposeContainer.uikit.kt | 11 +- .../ui/window/RenderingUIView.uikit.kt | 5 +- 7 files changed, 212 insertions(+), 218 deletions(-) delete mode 100644 compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/LocalUIKitInteropContext.kt diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/LocalUIKitInteropContext.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/LocalUIKitInteropContext.kt deleted file mode 100644 index 91ca720df8317..0000000000000 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/LocalUIKitInteropContext.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.compose.ui.interop - -import androidx.compose.runtime.staticCompositionLocalOf -import platform.Foundation.NSLock - -internal enum class UIKitInteropState { - BEGAN, UNCHANGED, ENDED -} - -internal enum class UIKitInteropViewHierarchyChange { - VIEW_ADDED, - VIEW_REMOVED -} - -/** - * Lambda containing changes to UIKit objects, which can be synchronized within [CATransaction] - */ -internal typealias UIKitInteropAction = () -> Unit - -internal interface UIKitInteropTransaction { - val actions: List - val state: UIKitInteropState - - companion object { - val empty = object : UIKitInteropTransaction { - override val actions: List - get() = emptyList() - - override val state: UIKitInteropState - get() = UIKitInteropState.UNCHANGED - } - } -} - -internal fun UIKitInteropTransaction.isEmpty() = actions.isEmpty() && state == UIKitInteropState.UNCHANGED -internal fun UIKitInteropTransaction.isNotEmpty() = !isEmpty() - -private class UIKitInteropMutableTransaction: UIKitInteropTransaction { - override val actions = mutableListOf() - override var state = UIKitInteropState.UNCHANGED - set(value) { - field = when (value) { - UIKitInteropState.UNCHANGED -> error("Can't assign UNCHANGED value explicitly") - UIKitInteropState.BEGAN -> { - when (field) { - UIKitInteropState.BEGAN -> error("Can't assign BEGAN twice in the same transaction") - UIKitInteropState.UNCHANGED -> value - UIKitInteropState.ENDED -> UIKitInteropState.UNCHANGED - } - } - UIKitInteropState.ENDED -> { - when (field) { - UIKitInteropState.BEGAN -> UIKitInteropState.UNCHANGED - UIKitInteropState.UNCHANGED -> value - UIKitInteropState.ENDED -> error("Can't assign ENDED twice in the same transaction") - } - } - } - } -} - -/** - * Class which can be used to add actions related to UIKit objects to be executed in sync with compose rendering, - * Addding deferred actions is threadsafe, but they will be executed in the order of their submission, and on the main thread. - */ -internal class UIKitInteropContext( - val requestRedraw: () -> Unit -) { - private val lock: NSLock = NSLock() - private var transaction = UIKitInteropMutableTransaction() - - /** - * Number of views, created by interop API and present in current view hierarchy - */ - private var viewsCount = 0 - set(value) { - require(value >= 0) - - field = value - } - - /** - * Add lambda to a list of commands which will be executed later in the same CATransaction, when the next rendered Compose frame is presented - */ - fun deferAction(hierarchyChange: UIKitInteropViewHierarchyChange? = null, action: () -> Unit) { - requestRedraw() - - lock.doLocked { - if (hierarchyChange == UIKitInteropViewHierarchyChange.VIEW_ADDED) { - if (viewsCount == 0) { - transaction.state = UIKitInteropState.BEGAN - } - viewsCount += 1 - } - - transaction.actions.add(action) - - if (hierarchyChange == UIKitInteropViewHierarchyChange.VIEW_REMOVED) { - viewsCount -= 1 - if (viewsCount == 0) { - transaction.state = UIKitInteropState.ENDED - } - } - } - } - - /** - * Return an object containing pending changes and reset internal storage - */ - internal fun retrieve(): UIKitInteropTransaction = - lock.doLocked { - val result = transaction - transaction = UIKitInteropMutableTransaction() - result - } -} - -internal inline fun NSLock.doLocked(block: () -> T): T { - lock() - - try { - return block() - } finally { - unlock() - } -} - -internal val LocalUIKitInteropContext = staticCompositionLocalOf { - error("CompositionLocal UIKitInteropContext not provided") -} diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt index c18fa568e6879..9fb9ac7154bcc 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt @@ -27,6 +27,8 @@ import kotlinx.cinterop.CValue import kotlinx.cinterop.readValue import platform.CoreGraphics.CGPoint import platform.CoreGraphics.CGRectZero +import platform.Foundation.NSLock +import platform.QuartzCore.CATransaction import platform.UIKit.UIEvent import platform.UIKit.UIView @@ -38,18 +40,135 @@ internal val LocalUIKitInteropContainer = staticCompositionLocalOf Unit + +internal interface UIKitInteropTransaction { + val actions: List + val state: UIKitInteropState + + companion object { + val empty = object : UIKitInteropTransaction { + override val actions: List + get() = emptyList() + + override val state: UIKitInteropState + get() = UIKitInteropState.UNCHANGED + } + } +} + +internal fun UIKitInteropTransaction.isEmpty() = actions.isEmpty() && state == UIKitInteropState.UNCHANGED +internal fun UIKitInteropTransaction.isNotEmpty() = !isEmpty() + +private class UIKitInteropMutableTransaction: UIKitInteropTransaction { + override val actions = mutableListOf() + override var state = UIKitInteropState.UNCHANGED + set(value) { + field = when (value) { + UIKitInteropState.UNCHANGED -> error("Can't assign UNCHANGED value explicitly") + UIKitInteropState.BEGAN -> { + when (field) { + UIKitInteropState.BEGAN -> error("Can't assign BEGAN twice in the same transaction") + UIKitInteropState.UNCHANGED -> value + UIKitInteropState.ENDED -> UIKitInteropState.UNCHANGED + } + } + UIKitInteropState.ENDED -> { + when (field) { + UIKitInteropState.BEGAN -> UIKitInteropState.UNCHANGED + UIKitInteropState.UNCHANGED -> value + UIKitInteropState.ENDED -> error("Can't assign ENDED twice in the same transaction") + } + } + } + } +} + /** * A container that controls interop views/components. */ internal class UIKitInteropContainer( - private val interopContext: UIKitInteropContext + val requestRedraw: () -> Unit ): InteropContainer { val containerView: UIView = UIKitInteropContainerView() override var rootModifier: TrackInteropModifierNode? = null override var interopViews = mutableSetOf() private set - override fun placeInteropView(nativeView: UIView) = interopContext.deferAction { + private val lock: NSLock = NSLock() + private var transaction = UIKitInteropMutableTransaction() + + /** + * Number of views, created by interop API and present in current view hierarchy + */ + private var viewsCount = 0 + set(value) { + require(value >= 0) + + field = value + } + + /** + * Dispose by immediately executing all UIKit interop actions that can't be deferred to be + * synchronized with rendering because scene will never be rendered past that moment. + */ + fun dispose() { + val lastTransaction = retrieveTransaction() + + for (action in lastTransaction.actions) { + action.invoke() + } + } + + /** + * Add lambda to a list of commands which will be executed later in the same CATransaction, when the next rendered Compose frame is presented + */ + fun deferAction(hierarchyChange: UIKitInteropViewHierarchyChange? = null, action: () -> Unit) { + requestRedraw() + + lock.doLocked { + if (hierarchyChange == UIKitInteropViewHierarchyChange.VIEW_ADDED) { + if (viewsCount == 0) { + transaction.state = UIKitInteropState.BEGAN + } + viewsCount += 1 + } + + transaction.actions.add(action) + + if (hierarchyChange == UIKitInteropViewHierarchyChange.VIEW_REMOVED) { + viewsCount -= 1 + if (viewsCount == 0) { + transaction.state = UIKitInteropState.ENDED + } + } + } + } + + /** + * Return an object containing pending changes and reset internal storage + */ + fun retrieveTransaction(): UIKitInteropTransaction = + lock.doLocked { + val result = transaction + transaction = UIKitInteropMutableTransaction() + result + } + + override fun placeInteropView(nativeView: UIView) = deferAction { val index = countInteropComponentsBelow(nativeView) if (nativeView in interopViews) { // Place might be called multiple times @@ -90,3 +209,13 @@ internal fun Modifier.trackUIKitInterop( container = container, nativeView = view ) + +internal inline fun NSLock.doLocked(block: () -> T): T { + lock() + + try { + return block() + } finally { + unlock() + } +} diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt index 0ed9e4883554c..19d821bc3470e 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt @@ -60,6 +60,7 @@ import platform.UIKit.didMoveToParentViewController import platform.UIKit.removeFromParentViewController import platform.UIKit.willMoveToParentViewController import androidx.compose.ui.uikit.utils.CMPInteropWrappingView +import androidx.compose.ui.unit.Density import kotlinx.cinterop.readValue import platform.CoreGraphics.CGRectZero @@ -128,14 +129,12 @@ private fun UIKitInteropLayout( update: (T) -> Unit, background: Color, componentHandler: InteropComponentHandler, - onResize: (T, rect: CValue) -> Unit, interactive: Boolean, accessibilityEnabled: Boolean, ) { val density = LocalDensity.current var rectInPixels by remember { mutableStateOf(IntRect(0, 0, 0, 0)) } var localToWindowOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } - val interopContext = LocalUIKitInteropContext.current val interopContainer = LocalUIKitInteropContainer.current val finalModifier = modifier @@ -143,25 +142,12 @@ private fun UIKitInteropLayout( localToWindowOffset = coordinates.positionInRoot().round() val newRectInPixels = IntRect(localToWindowOffset, coordinates.size) if (rectInPixels != newRectInPixels) { - val rect = newRectInPixels.toRect().toDpRect(density) - - interopContext.deferAction { - componentHandler.wrappingView.setFrame(rect.asCGRect()) - } + componentHandler.updateRect( + from = rectInPixels, + to = newRectInPixels, + density = density + ) - if (rectInPixels.width != newRectInPixels.width || rectInPixels.height != newRectInPixels.height) { - interopContext.deferAction { - onResize( - componentHandler.component, - CGRectMake( - x = 0.0, - y = 0.0, - width = rect.width.value.toDouble(), - height = rect.height.value.toDouble() - ), - ) - } - } rectInPixels = newRectInPixels } } @@ -181,27 +167,27 @@ private fun UIKitInteropLayout( ) DisposableEffect(Unit) { - componentHandler.initialize(interopContext, update) + componentHandler.initialize(update) - interopContext.deferAction(UIKitInteropViewHierarchyChange.VIEW_ADDED) { + interopContainer.deferAction(UIKitInteropViewHierarchyChange.VIEW_ADDED) { componentHandler.addToHierarchy() } onDispose { - interopContext.deferAction(UIKitInteropViewHierarchyChange.VIEW_REMOVED) { + interopContainer.deferAction(UIKitInteropViewHierarchyChange.VIEW_REMOVED) { componentHandler.removeFromHierarchy() } } } LaunchedEffect(background) { - interopContext.deferAction { + interopContainer.deferAction { componentHandler.setBackgroundColor(background) } } SideEffect { - componentHandler.update = update + componentHandler.setUpdate(update) } } @@ -249,6 +235,7 @@ fun UIKitView( InteropViewHandler( createView = factory, interopContainer = interopContainer, + onResize = onResize, onRelease = onRelease ) } @@ -258,7 +245,6 @@ fun UIKitView( update = update, background = background, componentHandler = handler, - onResize = onResize, interactive = interactive, accessibilityEnabled = accessibilityEnabled ) @@ -312,6 +298,7 @@ fun UIKitViewController( createViewController = factory, interopContainer = interopContainer, rootViewController = rootViewController, + onResize = onResize, onRelease = onRelease ) } @@ -321,7 +308,6 @@ fun UIKitViewController( update = update, background = background, componentHandler = handler, - onResize = onResize, interactive = interactive, accessibilityEnabled = accessibilityEnabled ) @@ -335,27 +321,52 @@ private abstract class InteropComponentHandler( // https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1) val createComponent: () -> T, val interopContainer: UIKitInteropContainer, - val onRelease: (T) -> Unit + val onResize: (T, rect: CValue) -> Unit, + val onRelease: (T) -> Unit, ) { val wrappingView = InteropWrappingView() lateinit var component: T private lateinit var updater: Updater /** - * Access the [update] lambda. - * Lambda is immediately executed when set anew. + * Set the [Updater.update] lambda. + * Lambda is immediately executed after setting. * @see Updater.performUpdate */ - var update: (T) -> Unit - get() = updater.update - set(value) { - updater.update = value + fun setUpdate(block: (T) -> Unit) { + updater.update = block + } + + /** + * Set the frame of the wrapping view. + */ + fun updateRect(from: IntRect, to: IntRect, density: Density) { + val dpRect = to.toRect().toDpRect(density) + + interopContainer.deferAction { + wrappingView.setFrame(dpRect.asCGRect()) + } + + if (from.size != to.size) { + interopContainer.deferAction { + // The actual component created by the user is resized here using the provided callback. + onResize( + component, + CGRectMake( + x = 0.0, + y = 0.0, + width = dpRect.width.value.toDouble(), + height = dpRect.height.value.toDouble() + ), + ) + } } + } - fun initialize(interopContext: UIKitInteropContext, update: (T) -> Unit) { + fun initialize(update: (T) -> Unit) { component = createComponent() updater = Updater(component, update) { - interopContext.deferAction(action = it) + interopContainer.deferAction(action = it) } } @@ -389,8 +400,9 @@ private abstract class InteropComponentHandler( private class InteropViewHandler( createView: () -> T, interopContainer: UIKitInteropContainer, + onResize: (T, rect: CValue) -> Unit, onRelease: (T) -> Unit -) : InteropComponentHandler(createView, interopContainer, onRelease) { +) : InteropComponentHandler(createView, interopContainer, onResize, onRelease) { override fun addToHierarchy() { addViewToHierarchy(component) } @@ -404,8 +416,9 @@ private class InteropViewControllerHandler( createViewController: () -> T, interopContainer: UIKitInteropContainer, private val rootViewController: UIViewController, + onResize: (T, rect: CValue) -> Unit, onRelease: (T) -> Unit -) : InteropComponentHandler(createViewController, interopContainer, onRelease) { +) : InteropComponentHandler(createViewController, interopContainer, onResize, onRelease) { override fun addToHierarchy() { rootViewController.addChildViewController(component) addViewToHierarchy(component.view) @@ -428,7 +441,7 @@ private class InteropViewControllerHandler( * @param deferAction The lambda to register [update] execution to defer it in order to sync it with * Compose rendering. The aim of this is to make visual changes to UIKit and Compose * simultaneously. - * @see [UIKitInteropContext] and [UIKitInteropTransaction] for more details. + * @see [UIKitInteropContainer] and [UIKitInteropTransaction] for more details. */ private class Updater( private val component: T, diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt index 3eb0db582c516..52d1596f4c907 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt @@ -33,9 +33,7 @@ import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerId import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.interop.LocalUIKitInteropContainer -import androidx.compose.ui.interop.LocalUIKitInteropContext import androidx.compose.ui.interop.UIKitInteropContainer -import androidx.compose.ui.interop.UIKitInteropContext import androidx.compose.ui.node.TrackInteropContainer import androidx.compose.ui.platform.AccessibilityMediator import androidx.compose.ui.platform.AccessibilitySyncOptions @@ -206,7 +204,7 @@ internal class ComposeSceneMediator( */ private val measureDrawLayerBounds: Boolean = false, val coroutineContext: CoroutineContext, - private val renderingUIViewFactory: (UIKitInteropContext, SkikoRenderDelegate) -> RenderingUIView, + private val renderingUIViewFactory: (UIKitInteropContainer, SkikoRenderDelegate) -> RenderingUIView, composeSceneFactory: ( invalidate: () -> Unit, platformContext: PlatformContext, @@ -249,7 +247,7 @@ internal class ComposeSceneMediator( val focusManager get() = scene.focusManager private val renderingView by lazy { - renderingUIViewFactory(interopContext, renderDelegate) + renderingUIViewFactory(interopContainer, renderDelegate) } private val applicationForegroundStateListener = ApplicationForegroundStateListener { isForeground -> @@ -262,19 +260,16 @@ internal class ComposeSceneMediator( } /** - * view, that contains [interopViewContainer] and [interactionView] and is added to [container] + * view, that contains [interopContainer] and [interactionView] and is added to [container] */ private val rootView = ComposeSceneMediatorRootUIView() - - private val interopContext = UIKitInteropContext( - requestRedraw = ::onComposeSceneInvalidate - ) - /** - * Container for UIKitView and UIKitViewController + * Container for managing UIKitView and UIKitViewController */ - private val interopViewContainer = UIKitInteropContainer(interopContext) + private val interopContainer = UIKitInteropContainer( + requestRedraw = ::onComposeSceneInvalidate + ) private val interactionBounds: IntRect get() { val boundsLayout = _layout as? SceneLayout.Bounds @@ -431,10 +426,10 @@ internal class ComposeSceneMediator( getConstraintsToFillParent(rootView, container) ) - interopViewContainer.containerView.translatesAutoresizingMaskIntoConstraints = false - rootView.addSubview(interopViewContainer.containerView) + interopContainer.containerView.translatesAutoresizingMaskIntoConstraints = false + rootView.addSubview(interopContainer.containerView) NSLayoutConstraint.activateConstraints( - getConstraintsToFillParent(interopViewContainer.containerView, rootView) + getConstraintsToFillParent(interopContainer.containerView, rootView) ) interactionView.translatesAutoresizingMaskIntoConstraints = false @@ -460,7 +455,7 @@ internal class ComposeSceneMediator( */ if (renderingView.isReadyToShowContent.value) { ProvideComposeSceneMediatorCompositionLocals { - interopViewContainer.TrackInteropContainer( + interopContainer.TrackInteropContainer( content = content ) } @@ -486,8 +481,7 @@ internal class ComposeSceneMediator( @Composable private fun ProvideComposeSceneMediatorCompositionLocals(content: @Composable () -> Unit) = CompositionLocalProvider( - LocalUIKitInteropContext provides interopContext, - LocalUIKitInteropContainer provides interopViewContainer, + LocalUIKitInteropContainer provides interopContainer, LocalKeyboardOverlapHeight provides keyboardOverlapHeightState.value, LocalSafeArea provides safeAreaState.value, LocalLayoutMargins provides layoutMarginsState.value, @@ -505,9 +499,7 @@ internal class ComposeSceneMediator( interactionView.removeFromSuperview() renderingView.removeFromSuperview() scene.close() - // After scene is disposed all UIKit interop actions can't be deferred to be synchronized with rendering - // Thus they need to be executed now. - interopContext.retrieve().actions.forEach { it.invoke() } + interopContainer.dispose() } private fun onComposeSceneInvalidate() = renderingView.needRedraw() diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt index d1f9b8aba2e0c..0e280be76aad0 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.pointer.PointerButton import androidx.compose.ui.input.pointer.PointerEventType -import androidx.compose.ui.interop.UIKitInteropContext +import androidx.compose.ui.interop.UIKitInteropContainer import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.PlatformWindowContext import androidx.compose.ui.skiko.RecordDrawRectRenderDecorator @@ -149,10 +149,12 @@ internal class UIViewComposeSceneLayer( composeContainer.attachLayer(this) } - private fun createSkikoUIView(interopContext: UIKitInteropContext, renderDelegate: SkikoRenderDelegate): RenderingUIView = + private fun createSkikoUIView(interopContainer: UIKitInteropContainer, renderDelegate: SkikoRenderDelegate): RenderingUIView = RenderingUIView( - interopContext = interopContext, - renderDelegate = recordDrawBounds(renderDelegate) + renderDelegate = recordDrawBounds(renderDelegate), + retrieveInteropTransaction = { + interopContainer.retrieveTransaction() + } ).apply { opaque = false } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt index 768f3c6e704d2..c5baaa54b83e3 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.LocalSystemTheme import androidx.compose.ui.SystemTheme import androidx.compose.ui.hapticfeedback.CupertinoHapticFeedback import androidx.compose.ui.interop.LocalUIViewController -import androidx.compose.ui.interop.UIKitInteropContext +import androidx.compose.ui.interop.UIKitInteropContainer import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalInternalViewModelStoreOwner @@ -315,10 +315,15 @@ internal class ComposeContainer( @OptIn(ExperimentalComposeApi::class) private fun createSkikoUIView( - interopContext: UIKitInteropContext, + interopContainer: UIKitInteropContainer, renderRelegate: SkikoRenderDelegate ): RenderingUIView = - RenderingUIView(interopContext, renderRelegate).apply { + RenderingUIView( + renderDelegate = renderRelegate, + retrieveInteropTransaction = { + interopContainer.retrieveTransaction() + } + ).apply { opaque = configuration.opaque } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt index 6d336de983ba3..d3c5bd501ef95 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt @@ -19,7 +19,6 @@ package androidx.compose.ui.window import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.interop.UIKitInteropContext import androidx.compose.ui.interop.UIKitInteropTransaction import kotlin.math.floor import kotlin.math.roundToLong @@ -35,8 +34,8 @@ import platform.QuartzCore.CAMetalLayer import platform.UIKit.* internal class RenderingUIView( - private val interopContext: UIKitInteropContext, private val renderDelegate: SkikoRenderDelegate, + private val retrieveInteropTransaction: () -> UIKitInteropTransaction, ) : UIView( frame = CGRectMake( x = 0.0, @@ -67,7 +66,7 @@ internal class RenderingUIView( } override fun retrieveInteropTransaction(): UIKitInteropTransaction = - interopContext.retrieve() + retrieveInteropTransaction() } ) From a263c34082d58b7fd578bc84fd284016f42bb5ab Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Mon, 24 Jun 2024 12:43:25 +0200 Subject: [PATCH 10/18] Rename and update docs --- .../compose/ui/scene/UIViewComposeSceneLayer.uikit.kt | 2 +- .../androidx/compose/ui/window/ComposeContainer.uikit.kt | 2 +- .../androidx/compose/ui/window/MetalRedrawer.uikit.kt | 6 +++--- .../androidx/compose/ui/window/RenderingUIView.uikit.kt | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt index 0e280be76aad0..cf294a94f4402 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt @@ -152,7 +152,7 @@ internal class UIViewComposeSceneLayer( private fun createSkikoUIView(interopContainer: UIKitInteropContainer, renderDelegate: SkikoRenderDelegate): RenderingUIView = RenderingUIView( renderDelegate = recordDrawBounds(renderDelegate), - retrieveInteropTransaction = { + setupInteropTransaction = { interopContainer.retrieveTransaction() } ).apply { diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt index c5baaa54b83e3..7d00061a6a165 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt @@ -320,7 +320,7 @@ internal class ComposeContainer( ): RenderingUIView = RenderingUIView( renderDelegate = renderRelegate, - retrieveInteropTransaction = { + setupInteropTransaction = { interopContainer.retrieveTransaction() } ).apply { diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/MetalRedrawer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/MetalRedrawer.uikit.kt index 94e06b695e9c6..608b86ee5c76c 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/MetalRedrawer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/MetalRedrawer.uikit.kt @@ -115,10 +115,10 @@ internal interface MetalRedrawerCallbacks { fun render(canvas: Canvas, targetTimestamp: NSTimeInterval) /** - * Retrieve a transaction object, containing a list of pending actions + * Create a transaction from the list of pending actions * that need to be synchronized with Metal rendering using CATransaction mechanism. */ - fun retrieveInteropTransaction(): UIKitInteropTransaction + fun setupInteropTransaction(): UIKitInteropTransaction } internal class InflightCommandBuffers( @@ -357,7 +357,7 @@ internal class MetalRedrawer( return@autoreleasepool } - val interopTransaction = callbacks.retrieveInteropTransaction() + val interopTransaction = callbacks.setupInteropTransaction() if (interopTransaction.state == UIKitInteropState.BEGAN) { isInteropActive = true } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt index d3c5bd501ef95..577accc48ab51 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt @@ -35,7 +35,7 @@ import platform.UIKit.* internal class RenderingUIView( private val renderDelegate: SkikoRenderDelegate, - private val retrieveInteropTransaction: () -> UIKitInteropTransaction, + private val setupInteropTransaction: () -> UIKitInteropTransaction, ) : UIView( frame = CGRectMake( x = 0.0, @@ -65,8 +65,8 @@ internal class RenderingUIView( renderDelegate.onRender(canvas, _width.toInt(), _height.toInt(), targetTimestamp.toNanoSeconds()) } - override fun retrieveInteropTransaction(): UIKitInteropTransaction = - retrieveInteropTransaction() + override fun setupInteropTransaction(): UIKitInteropTransaction = + this@RenderingUIView.setupInteropTransaction() } ) From 96977d122289d69948ba98d3514b3dde7e508dea Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Mon, 24 Jun 2024 12:45:36 +0200 Subject: [PATCH 11/18] Rename and update docs --- .../compose/ui/scene/UIViewComposeSceneLayer.uikit.kt | 2 +- .../androidx/compose/ui/window/ComposeContainer.uikit.kt | 2 +- .../androidx/compose/ui/window/MetalRedrawer.uikit.kt | 6 +++--- .../androidx/compose/ui/window/RenderingUIView.uikit.kt | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt index cf294a94f4402..0e280be76aad0 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt @@ -152,7 +152,7 @@ internal class UIViewComposeSceneLayer( private fun createSkikoUIView(interopContainer: UIKitInteropContainer, renderDelegate: SkikoRenderDelegate): RenderingUIView = RenderingUIView( renderDelegate = recordDrawBounds(renderDelegate), - setupInteropTransaction = { + retrieveInteropTransaction = { interopContainer.retrieveTransaction() } ).apply { diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt index 7d00061a6a165..c5baaa54b83e3 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt @@ -320,7 +320,7 @@ internal class ComposeContainer( ): RenderingUIView = RenderingUIView( renderDelegate = renderRelegate, - setupInteropTransaction = { + retrieveInteropTransaction = { interopContainer.retrieveTransaction() } ).apply { diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/MetalRedrawer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/MetalRedrawer.uikit.kt index 608b86ee5c76c..ed33a407f22ec 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/MetalRedrawer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/MetalRedrawer.uikit.kt @@ -115,10 +115,10 @@ internal interface MetalRedrawerCallbacks { fun render(canvas: Canvas, targetTimestamp: NSTimeInterval) /** - * Create a transaction from the list of pending actions + * Create an interop transaction by flushing the list of all pending actions * that need to be synchronized with Metal rendering using CATransaction mechanism. */ - fun setupInteropTransaction(): UIKitInteropTransaction + fun retrieveInteropTransaction(): UIKitInteropTransaction } internal class InflightCommandBuffers( @@ -357,7 +357,7 @@ internal class MetalRedrawer( return@autoreleasepool } - val interopTransaction = callbacks.setupInteropTransaction() + val interopTransaction = callbacks.retrieveInteropTransaction() if (interopTransaction.state == UIKitInteropState.BEGAN) { isInteropActive = true } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt index 577accc48ab51..58582367aced2 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt @@ -35,7 +35,7 @@ import platform.UIKit.* internal class RenderingUIView( private val renderDelegate: SkikoRenderDelegate, - private val setupInteropTransaction: () -> UIKitInteropTransaction, + private val retrieveInteropTransaction: () -> UIKitInteropTransaction, ) : UIView( frame = CGRectMake( x = 0.0, @@ -65,8 +65,8 @@ internal class RenderingUIView( renderDelegate.onRender(canvas, _width.toInt(), _height.toInt(), targetTimestamp.toNanoSeconds()) } - override fun setupInteropTransaction(): UIKitInteropTransaction = - this@RenderingUIView.setupInteropTransaction() + override fun retrieveInteropTransaction(): UIKitInteropTransaction = + this@RenderingUIView.retrieveInteropTransaction() } ) From d360ead209c640ea0834c4de8c2476825813fd75 Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Mon, 24 Jun 2024 12:46:24 +0200 Subject: [PATCH 12/18] Remove dead code --- .../compose/ui/interop/UIKitInteropContainer.uikit.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt index 9fb9ac7154bcc..6f4b1bb41ab07 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt @@ -58,16 +58,6 @@ internal typealias UIKitInteropAction = () -> Unit internal interface UIKitInteropTransaction { val actions: List val state: UIKitInteropState - - companion object { - val empty = object : UIKitInteropTransaction { - override val actions: List - get() = emptyList() - - override val state: UIKitInteropState - get() = UIKitInteropState.UNCHANGED - } - } } internal fun UIKitInteropTransaction.isEmpty() = actions.isEmpty() && state == UIKitInteropState.UNCHANGED From d34c7758db6fb54d6e652c6ecf0e860604444fe9 Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Fri, 10 May 2024 09:06:06 +0200 Subject: [PATCH 13/18] Refactor further --- .../compose/ui/interop/UIKitView.uikit.kt | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt index 19d821bc3470e..15eac8eceae25 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt @@ -167,23 +167,15 @@ private fun UIKitInteropLayout( ) DisposableEffect(Unit) { - componentHandler.initialize(update) - - interopContainer.deferAction(UIKitInteropViewHierarchyChange.VIEW_ADDED) { - componentHandler.addToHierarchy() - } + componentHandler.onStart(update) onDispose { - interopContainer.deferAction(UIKitInteropViewHierarchyChange.VIEW_REMOVED) { - componentHandler.removeFromHierarchy() - } + componentHandler.onStop() } } LaunchedEffect(background) { - interopContainer.deferAction { - componentHandler.setBackgroundColor(background) - } + componentHandler.onBackgroundColorChange(background) } SideEffect { @@ -363,14 +355,24 @@ private abstract class InteropComponentHandler( } } - fun initialize(update: (T) -> Unit) { + fun onStart(initialUpdateBlock: (T) -> Unit) { component = createComponent() - updater = Updater(component, update) { + updater = Updater(component, initialUpdateBlock) { interopContainer.deferAction(action = it) } + + interopContainer.deferAction(UIKitInteropViewHierarchyChange.VIEW_ADDED) { + addToHierarchy() + } + } + + fun onStop() { + interopContainer.deferAction(UIKitInteropViewHierarchyChange.VIEW_REMOVED) { + removeFromHierarchy() + } } - fun setBackgroundColor(color: Color) { + fun onBackgroundColorChange(color: Color) = interopContainer.deferAction { if (color == Color.Unspecified) { wrappingView.backgroundColor = interopContainer.containerView.backgroundColor } else { From 9859fff30cbdf4949caad1665fbd24b77a6342e1 Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Tue, 25 Jun 2024 10:12:40 +0200 Subject: [PATCH 14/18] Change window rect calculation logic --- .../compose/ui/interop/UIKitView.uikit.kt | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt index 15eac8eceae25..7464faf04b50f 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.InteropViewCatchPointerModifier import androidx.compose.ui.layout.EmptyLayout +import androidx.compose.ui.layout.findRootCoordinates import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalDensity @@ -61,6 +62,7 @@ import platform.UIKit.removeFromParentViewController import platform.UIKit.willMoveToParentViewController import androidx.compose.ui.uikit.utils.CMPInteropWrappingView import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.roundToIntRect import kotlinx.cinterop.readValue import platform.CoreGraphics.CGRectZero @@ -133,26 +135,26 @@ private fun UIKitInteropLayout( accessibilityEnabled: Boolean, ) { val density = LocalDensity.current - var rectInPixels by remember { mutableStateOf(IntRect(0, 0, 0, 0)) } - var localToWindowOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } val interopContainer = LocalUIKitInteropContainer.current val finalModifier = modifier .onGloballyPositioned { coordinates -> - localToWindowOffset = coordinates.positionInRoot().round() - val newRectInPixels = IntRect(localToWindowOffset, coordinates.size) - if (rectInPixels != newRectInPixels) { - componentHandler.updateRect( - from = rectInPixels, - to = newRectInPixels, - density = density + val rootCoordinates = coordinates.findRootCoordinates() + + // TODO: perform proper clipping of underlying view with `clipBounds` set to true + val bounds = rootCoordinates + .localBoundingBoxOf( + sourceCoordinates = coordinates, + clipBounds = false ) - rectInPixels = newRectInPixels - } + componentHandler.updateRect( + to = bounds.roundToIntRect(), + density = density + ) } .drawBehind { - // Clear interop area to make visible the component under our canvas. + // Paint the rectangle behind with transparent color to let our interop shine through drawRect( color = Color.Transparent, blendMode = BlendMode.Clear @@ -316,6 +318,10 @@ private abstract class InteropComponentHandler( val onResize: (T, rect: CValue) -> Unit, val onRelease: (T) -> Unit, ) { + /** + * The coordinates + */ + private var currentRect: IntRect? = null val wrappingView = InteropWrappingView() lateinit var component: T private lateinit var updater: Updater @@ -332,14 +338,20 @@ private abstract class InteropComponentHandler( /** * Set the frame of the wrapping view. */ - fun updateRect(from: IntRect, to: IntRect, density: Density) { + fun updateRect(to: IntRect, density: Density) { + if (currentRect == to) { + return + } + val dpRect = to.toRect().toDpRect(density) interopContainer.deferAction { wrappingView.setFrame(dpRect.asCGRect()) } - if (from.size != to.size) { + + // Only call onResize if the actual size changes. + if (currentRect?.size != to.size) { interopContainer.deferAction { // The actual component created by the user is resized here using the provided callback. onResize( @@ -353,6 +365,8 @@ private abstract class InteropComponentHandler( ) } } + + currentRect = to } fun onStart(initialUpdateBlock: (T) -> Unit) { From 1948270000a14d83790c393419a9e567ecb77d0d Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Tue, 25 Jun 2024 13:15:53 +0200 Subject: [PATCH 15/18] Refactor placement logic --- .../ui/awt/SwingInteropContainer.desktop.kt | 2 +- .../compose/ui/node/InteropContainer.skiko.kt | 1 - .../ui/interop/UIKitInteropContainer.uikit.kt | 88 ++++++++----------- .../compose/ui/interop/UIKitView.uikit.kt | 59 +++++-------- 4 files changed, 58 insertions(+), 92 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/SwingInteropContainer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/SwingInteropContainer.desktop.kt index c4041a2ae9456..cfa8af50e9ace 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/SwingInteropContainer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/SwingInteropContainer.desktop.kt @@ -110,7 +110,7 @@ internal class SwingInteropContainer( container.repaint() } - override fun removeInteropView(nativeView: InteropComponent) { + fun removeInteropView(nativeView: InteropComponent) { val component = nativeView.container container.remove(component) interopComponents.remove(component) diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/InteropContainer.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/InteropContainer.skiko.kt index dd54b1dbf121e..dc46bd7d816b2 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/InteropContainer.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/InteropContainer.skiko.kt @@ -31,7 +31,6 @@ internal interface InteropContainer { val interopViews: Set fun placeInteropView(nativeView: T) - fun removeInteropView(nativeView: T) } /** diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt index 6f4b1bb41ab07..2529caf4a02fb 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt @@ -45,11 +45,6 @@ internal enum class UIKitInteropState { BEGAN, UNCHANGED, ENDED } -internal enum class UIKitInteropViewHierarchyChange { - VIEW_ADDED, - VIEW_REMOVED -} - /** * Lambda containing changes to UIKit objects, which can be synchronized within [CATransaction] */ @@ -60,10 +55,12 @@ internal interface UIKitInteropTransaction { val state: UIKitInteropState } -internal fun UIKitInteropTransaction.isEmpty() = actions.isEmpty() && state == UIKitInteropState.UNCHANGED +internal fun UIKitInteropTransaction.isEmpty() = + actions.isEmpty() && state == UIKitInteropState.UNCHANGED + internal fun UIKitInteropTransaction.isNotEmpty() = !isEmpty() -private class UIKitInteropMutableTransaction: UIKitInteropTransaction { +private class UIKitInteropMutableTransaction : UIKitInteropTransaction { override val actions = mutableListOf() override var state = UIKitInteropState.UNCHANGED set(value) { @@ -92,25 +89,14 @@ private class UIKitInteropMutableTransaction: UIKitInteropTransaction { */ internal class UIKitInteropContainer( val requestRedraw: () -> Unit -): InteropContainer { +) : InteropContainer { val containerView: UIView = UIKitInteropContainerView() override var rootModifier: TrackInteropModifierNode? = null override var interopViews = mutableSetOf() private set - private val lock: NSLock = NSLock() private var transaction = UIKitInteropMutableTransaction() - /** - * Number of views, created by interop API and present in current view hierarchy - */ - private var viewsCount = 0 - set(value) { - require(value >= 0) - - field = value - } - /** * Dispose by immediately executing all UIKit interop actions that can't be deferred to be * synchronized with rendering because scene will never be rendered past that moment. @@ -126,56 +112,52 @@ internal class UIKitInteropContainer( /** * Add lambda to a list of commands which will be executed later in the same CATransaction, when the next rendered Compose frame is presented */ - fun deferAction(hierarchyChange: UIKitInteropViewHierarchyChange? = null, action: () -> Unit) { + fun deferAction(action: () -> Unit) { requestRedraw() - lock.doLocked { - if (hierarchyChange == UIKitInteropViewHierarchyChange.VIEW_ADDED) { - if (viewsCount == 0) { - transaction.state = UIKitInteropState.BEGAN - } - viewsCount += 1 - } - - transaction.actions.add(action) - - if (hierarchyChange == UIKitInteropViewHierarchyChange.VIEW_REMOVED) { - viewsCount -= 1 - if (viewsCount == 0) { - transaction.state = UIKitInteropState.ENDED - } - } - } + transaction.actions.add(action) } /** * Return an object containing pending changes and reset internal storage */ - fun retrieveTransaction(): UIKitInteropTransaction = - lock.doLocked { - val result = transaction - transaction = UIKitInteropMutableTransaction() - result - } + fun retrieveTransaction(): UIKitInteropTransaction { + val result = transaction + transaction = UIKitInteropMutableTransaction() + return result + } - override fun placeInteropView(nativeView: UIView) = deferAction { + /** + * Counts the number of interop components before the given native view in the container. + * And updates the index of the native view in the container for proper Z-ordering. + * @see TrackInteropModifierNode.onPlaced + */ + override fun placeInteropView(nativeView: UIView) { val index = countInteropComponentsBelow(nativeView) - if (nativeView in interopViews) { - // Place might be called multiple times - nativeView.removeFromSuperview() - } else { - interopViews.add(nativeView) + + deferAction { + containerView.insertSubview(nativeView, index.toLong()) } - containerView.insertSubview(nativeView, index.toLong()) } - override fun removeInteropView(nativeView: UIView) { - nativeView.removeFromSuperview() + fun startTrackingInteropView(nativeView: UIView) { + if (interopViews.isEmpty()) { + transaction.state = UIKitInteropState.BEGAN + } + + interopViews.add(nativeView) + } + + fun stopTrackingInteropView(nativeView: UIView) { interopViews.remove(nativeView) + + if (interopViews.isEmpty()) { + transaction.state = UIKitInteropState.ENDED + } } } -private class UIKitInteropContainerView: UIView(CGRectZero.readValue()) { +private class UIKitInteropContainerView : UIView(CGRectZero.readValue()) { /** * We used a simple solution to make only this view not touchable. * Another view added to this container will be touchable. diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt index 7464faf04b50f..05e1200b14ba4 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt @@ -20,10 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateObserver import androidx.compose.ui.Modifier import androidx.compose.runtime.State @@ -34,21 +31,17 @@ import androidx.compose.ui.input.pointer.InteropViewCatchPointerModifier import androidx.compose.ui.layout.EmptyLayout import androidx.compose.ui.layout.findRootCoordinates import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.AccessibilityKey import androidx.compose.ui.semantics.SemanticsPropertyReceiver import androidx.compose.ui.semantics.semantics import androidx.compose.ui.uikit.toUIColor -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.asCGRect import androidx.compose.ui.unit.height -import androidx.compose.ui.unit.round import androidx.compose.ui.unit.toDpRect import androidx.compose.ui.unit.toRect import androidx.compose.ui.unit.width -import androidx.compose.ui.node.TrackInteropModifierNode import kotlinx.atomicfu.atomic import kotlinx.cinterop.CValue import platform.CoreGraphics.CGRect @@ -169,7 +162,7 @@ private fun UIKitInteropLayout( ) DisposableEffect(Unit) { - componentHandler.onStart(update) + componentHandler.onStart(initialUpdateBlock = update) onDispose { componentHandler.onStop() @@ -375,15 +368,20 @@ private abstract class InteropComponentHandler( interopContainer.deferAction(action = it) } - interopContainer.deferAction(UIKitInteropViewHierarchyChange.VIEW_ADDED) { - addToHierarchy() + interopContainer.startTrackingInteropView(wrappingView) + interopContainer.deferAction { + setupViewHierarchy() } } fun onStop() { - interopContainer.deferAction(UIKitInteropViewHierarchyChange.VIEW_REMOVED) { - removeFromHierarchy() + interopContainer.stopTrackingInteropView(wrappingView) + interopContainer.deferAction { + destroyViewHierarchy() } + + onRelease(component) + updater.dispose() } fun onBackgroundColorChange(color: Color) = interopContainer.deferAction { @@ -394,23 +392,8 @@ private abstract class InteropComponentHandler( } } - abstract fun addToHierarchy() - abstract fun removeFromHierarchy() - - /** - * Places the actual view a user constructed in factory to the [wrappingView] - * [wrappingView] will be added to the [UIKitInteropContainer] within [TrackInteropModifierNode] - */ - protected fun addViewToHierarchy(view: UIView) { - wrappingView.addSubview(view) - } - - protected fun removeViewFromHierarchy(view: UIView) { - view.removeFromSuperview() - interopContainer.removeInteropView(wrappingView) - updater.dispose() - onRelease(component) - } + abstract fun setupViewHierarchy() + abstract fun destroyViewHierarchy() } private class InteropViewHandler( @@ -419,12 +402,13 @@ private class InteropViewHandler( onResize: (T, rect: CValue) -> Unit, onRelease: (T) -> Unit ) : InteropComponentHandler(createView, interopContainer, onResize, onRelease) { - override fun addToHierarchy() { - addViewToHierarchy(component) + override fun setupViewHierarchy() { + interopContainer.containerView.addSubview(wrappingView) + wrappingView.addSubview(component) } - override fun removeFromHierarchy() { - removeViewFromHierarchy(component) + override fun destroyViewHierarchy() { + wrappingView.removeFromSuperview() } } @@ -435,15 +419,16 @@ private class InteropViewControllerHandler( onResize: (T, rect: CValue) -> Unit, onRelease: (T) -> Unit ) : InteropComponentHandler(createViewController, interopContainer, onResize, onRelease) { - override fun addToHierarchy() { + override fun setupViewHierarchy() { rootViewController.addChildViewController(component) - addViewToHierarchy(component.view) + interopContainer.containerView.addSubview(wrappingView) + wrappingView.addSubview(component.view) component.didMoveToParentViewController(rootViewController) } - override fun removeFromHierarchy() { + override fun destroyViewHierarchy() { component.willMoveToParentViewController(null) - removeViewFromHierarchy(component.view) + wrappingView.removeFromSuperview() component.removeFromParentViewController() } } From 9a9b1e2d57906037e9b3bf0dec689f7bfbfee33f Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Thu, 27 Jun 2024 09:32:55 +0200 Subject: [PATCH 16/18] Hide mutability of actions --- .../ui/interop/UIKitInteropContainer.uikit.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt index 2529caf4a02fb..c3f010463707d 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt @@ -61,7 +61,11 @@ internal fun UIKitInteropTransaction.isEmpty() = internal fun UIKitInteropTransaction.isNotEmpty() = !isEmpty() private class UIKitInteropMutableTransaction : UIKitInteropTransaction { - override val actions = mutableListOf() + private val _actions = mutableListOf() + + override val actions + get() = _actions + override var state = UIKitInteropState.UNCHANGED set(value) { field = when (value) { @@ -82,6 +86,10 @@ private class UIKitInteropMutableTransaction : UIKitInteropTransaction { } } } + + fun add(action: UIKitInteropAction) { + _actions.add(action) + } } /** @@ -115,7 +123,7 @@ internal class UIKitInteropContainer( fun deferAction(action: () -> Unit) { requestRedraw() - transaction.actions.add(action) + transaction.add(action) } /** From 7d0a4a7f8c7783c50d4425740a3e8c8145dfc166 Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Thu, 27 Jun 2024 10:42:06 +0200 Subject: [PATCH 17/18] Add documentation. --- .../ui/interop/UIKitInteropContainer.uikit.kt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt index c3f010463707d..7c19529aa9a40 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt @@ -41,6 +41,12 @@ internal val LocalUIKitInteropContainer = staticCompositionLocalOf Unit +/** + * A transaction containing changes to UIKit objects to be synchronized within [CATransaction] inside a + * renderer to make sure that changes in UIKit and Compose are visually simultaneous. + * [actions] contains a list of lambdas that will be executed in the same CATransaction. + * [state] defines if rendering strategy should be changed along with this transaction. + */ internal interface UIKitInteropTransaction { val actions: List val state: UIKitInteropState @@ -60,6 +72,12 @@ internal fun UIKitInteropTransaction.isEmpty() = internal fun UIKitInteropTransaction.isNotEmpty() = !isEmpty() +/** + * A mutable transaction managed by [UIKitInteropContainer] to collect changes to UIKit objects to be executed later. + * @see UIKitView + * @see UIKitViewController + * @see UIKitInteropContainer.deferAction + */ private class UIKitInteropMutableTransaction : UIKitInteropTransaction { private val _actions = mutableListOf() @@ -93,7 +111,9 @@ private class UIKitInteropMutableTransaction : UIKitInteropTransaction { } /** - * A container that controls interop views/components. + * A container that controls interop views/components. It's using a modifier of [TrackInteropModifierNode] + * to properly sort native interop elements and contains a logic for syncing changes to UIKit objects + * driven by Compose state changes with Compose rendering. */ internal class UIKitInteropContainer( val requestRedraw: () -> Unit From 2d0f2d82eeeb7ad17e3cd745ac31d844aade7299 Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Thu, 27 Jun 2024 11:35:16 +0200 Subject: [PATCH 18/18] Move NSLock --- .../compose/ui/interop/UIKitInteropContainer.uikit.kt | 10 ---------- .../androidx/compose/ui/window/MetalRedrawer.uikit.kt | 11 ++++++++++- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt index 7c19529aa9a40..f56078527fd4d 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropContainer.uikit.kt @@ -27,7 +27,6 @@ import kotlinx.cinterop.CValue import kotlinx.cinterop.readValue import platform.CoreGraphics.CGPoint import platform.CoreGraphics.CGRectZero -import platform.Foundation.NSLock import platform.QuartzCore.CATransaction import platform.UIKit.UIEvent import platform.UIKit.UIView @@ -210,12 +209,3 @@ internal fun Modifier.trackUIKitInterop( nativeView = view ) -internal inline fun NSLock.doLocked(block: () -> T): T { - lock() - - try { - return block() - } finally { - unlock() - } -} diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/MetalRedrawer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/MetalRedrawer.uikit.kt index ed33a407f22ec..638b0984f2e4f 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/MetalRedrawer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/MetalRedrawer.uikit.kt @@ -18,7 +18,6 @@ package androidx.compose.ui.window import androidx.compose.ui.interop.UIKitInteropState import androidx.compose.ui.interop.UIKitInteropTransaction -import androidx.compose.ui.interop.doLocked import androidx.compose.ui.interop.isNotEmpty import androidx.compose.ui.uikit.utils.CMPMetalDrawablesHandler import androidx.compose.ui.util.fastForEach @@ -492,3 +491,13 @@ private class DisplayLinkProxy( callback() } } + +private inline fun NSLock.doLocked(block: () -> T): T { + lock() + + try { + return block() + } finally { + unlock() + } +}