Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor UIKit interop implementation #1411

Merged
merged 18 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ internal interface InteropContainer<T> {
val interopViews: Set<T>

fun placeInteropView(nativeView: T)
fun removeInteropView(nativeView: T)
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import kotlinx.cinterop.CValue
import kotlinx.cinterop.readValue
import platform.CoreGraphics.CGPoint
import platform.CoreGraphics.CGRectZero
import platform.QuartzCore.CATransaction
import platform.UIKit.UIEvent
import platform.UIKit.UIView

Expand All @@ -38,35 +39,152 @@ internal val LocalUIKitInteropContainer = staticCompositionLocalOf<UIKitInteropC
error("UIKitInteropContainer not provided")
}


/**
* A container that controls interop views/components.
* Enum which is used to define if rendering strategy should be changed along with this transaction.
* If [BEGAN], it will wait until a next CATransaction on every frame and make the metal layer opaque.
* If [ENDED] it will fallback to the most efficient rendering strategy (opaque layer, no transaction waiting, asynchronous encoding and GPU-driven presentation).
* If [UNCHANGED] it will keep the current rendering strategy.
*/
internal enum class UIKitInteropState {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, interop and enum values are too generic.
Could you please add brief comment explaining why this enum used?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, sure

BEGAN, UNCHANGED, ENDED
}

/**
* Lambda containing changes to UIKit objects, which can be synchronized within [CATransaction]
*/
internal typealias UIKitInteropAction = () -> 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<UIKitInteropAction>
val state: UIKitInteropState
}

internal fun UIKitInteropTransaction.isEmpty() =
actions.isEmpty() && state == UIKitInteropState.UNCHANGED

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<UIKitInteropAction>()

override val actions
get() = _actions

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")
}
}
}
}

fun add(action: UIKitInteropAction) {
_actions.add(action)
}
}

/**
* 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(
private val interopContext: UIKitInteropContext
): InteropContainer<UIView> {
val requestRedraw: () -> Unit
) : InteropContainer<UIView> {
val containerView: UIView = UIKitInteropContainerView()
override var rootModifier: TrackInteropModifierNode<UIView>? = null
override var interopViews = mutableSetOf<UIView>()
private set

override fun placeInteropView(nativeView: UIView) = interopContext.deferAction {
private var transaction = UIKitInteropMutableTransaction()

/**
* 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(action: () -> Unit) {
requestRedraw()

transaction.add(action)
}

/**
* Return an object containing pending changes and reset internal storage
*/
fun retrieveTransaction(): UIKitInteropTransaction {
val result = transaction
transaction = UIKitInteropMutableTransaction()
return result
}

/**
* 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())
}
}

fun startTrackingInteropView(nativeView: UIView) {
if (interopViews.isEmpty()) {
transaction.state = UIKitInteropState.BEGAN
}
containerView.insertSubview(nativeView, index.toLong())

interopViews.add(nativeView)
}

override fun removeInteropView(nativeView: UIView) {
nativeView.removeFromSuperview()
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.
Expand All @@ -78,7 +196,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.
*/
Expand All @@ -89,3 +208,4 @@ internal fun Modifier.trackUIKitInterop(
container = container,
nativeView = view
)

Loading
Loading