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

iOS avoid redundant compositing #813

Merged
merged 23 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2cd2b99
Implement momentum-based logic in display link logic.
elijah-semyonov Sep 7, 2023
9bee4c6
Implement momentum-based logic in display link logic.
elijah-semyonov Sep 7, 2023
177b20b
Modify comment
elijah-semyonov Sep 7, 2023
1a9534a
Minor changes
elijah-semyonov Sep 8, 2023
2a96b34
Minor changes
elijah-semyonov Sep 8, 2023
75d36cc
Remove getMainDispatcher
elijah-semyonov Sep 8, 2023
483cb19
Remove redundant code
elijah-semyonov Sep 8, 2023
f9c456b
Add magic number explanation
elijah-semyonov Sep 12, 2023
1cebe87
Make transparent on demand to avoid compositing
elijah-semyonov Sep 12, 2023
8d1942e
Snapshot
elijah-semyonov Sep 13, 2023
1f58a72
Update call flow.
elijah-semyonov Sep 13, 2023
88f0e60
Merge branch 'jb-main' into es/ios-modify-invalidation-logic-2
elijah-semyonov Sep 13, 2023
8108901
Extract currentTargetTimestamp to separate member
elijah-semyonov Sep 13, 2023
3f3ed74
Make transparent on demand to avoid compositing
elijah-semyonov Sep 12, 2023
5c03411
Snapshot
elijah-semyonov Sep 13, 2023
9ed14e4
Merge remote-tracking branch 'origin/es/ios-avoid-redundant-compositi…
elijah-semyonov Sep 13, 2023
ac9f94f
Snapshot
elijah-semyonov Sep 13, 2023
419e41a
Merge branch 'jb-main' into es/ios-avoid-redundant-compositing
elijah-semyonov Sep 13, 2023
a5dca3a
Fix a minor issue
elijah-semyonov Sep 13, 2023
26d1a48
Update doc
elijah-semyonov Sep 13, 2023
eed5590
Update comment
elijah-semyonov Sep 13, 2023
ff77fce
Update comment
elijah-semyonov Sep 13, 2023
9aa04f8
Update comment
elijah-semyonov Sep 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,62 @@ 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]
*/
typealias UIKitInteropAction = () -> Unit
elijah-semyonov marked this conversation as resolved.
Show resolved Hide resolved

internal interface UIKitInteropTransaction {
val actions: List<UIKitInteropAction>
val state: UIKitInteropState

companion object {
val empty = object : UIKitInteropTransaction {
override val actions: List<UIKitInteropAction>
get() = listOf()

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<UIKitInteropAction>()
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.
Expand All @@ -27,29 +83,52 @@ internal class UIKitInteropContext(
val requestRedraw: () -> Unit
) {
private val lock: NSLock = NSLock()
private val actions = mutableListOf<() -> Unit>()
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(action: () -> Unit) {
fun deferAction(hierarchyChange: UIKitInteropViewHierarchyChange? = null, action: () -> Unit) {
requestRedraw()

lock.doLocked {
actions.add(action)
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 a copy of the list of [actions] and clear it.
* Return an object containing pending changes and reset internal storage
*/
internal fun getActionsAndClear(): List<() -> Unit> {
return lock.doLocked {
val result = actions.toList()
actions.clear()
internal fun retrieve(): UIKitInteropTransaction =
lock.doLocked {
val result = transaction
transaction = UIKitInteropMutableTransaction()
result
}
}
}

private inline fun <T> NSLock.doLocked(block: () -> T): T {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.round
import kotlinx.atomicfu.atomic
import kotlinx.cinterop.CValue
import kotlinx.cinterop.useContents
import platform.CoreGraphics.CGRect
import platform.CoreGraphics.CGRectMake
import platform.Foundation.NSThread
import platform.UIKit.NSStringFromCGRect
import platform.UIKit.UIColor
import platform.UIKit.UIView
import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_main_queue

private val STUB_CALLBACK_WITH_RECEIVER: Any.() -> Unit = {}
private val NoOpUpdate: UIView.() -> Unit = STUB_CALLBACK_WITH_RECEIVER
Expand Down Expand Up @@ -122,17 +122,19 @@ fun <T : UIView> UIKitView(

DisposableEffect(Unit) {
componentInfo.component = factory()
componentInfo.updater = Updater(componentInfo.component, update, interopContext::deferAction)
componentInfo.updater = Updater(componentInfo.component, update) {
interopContext.deferAction(action = it)
}

interopContext.deferAction {
interopContext.deferAction(UIKitInteropViewHierarchyChange.VIEW_ADDED) {
componentInfo.container = UIView().apply {
addSubview(componentInfo.component)
}
root.insertSubview(componentInfo.container, 0)
}

onDispose {
interopContext.deferAction {
interopContext.deferAction(UIKitInteropViewHierarchyChange.VIEW_REMOVED) {
componentInfo.container.removeFromSuperview()
componentInfo.updater.dispose()
onRelease(componentInfo.component)
Expand Down Expand Up @@ -234,4 +236,4 @@ private class Updater<T : UIView>(
snapshotObserver.clear()
isDisposed = true
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import androidx.compose.ui.input.pointer.toCompose
import androidx.compose.ui.interop.LocalLayerContainer
import androidx.compose.ui.interop.LocalUIKitInteropContext
import androidx.compose.ui.interop.LocalUIViewController
import androidx.compose.ui.interop.UIKitInteropAction
import androidx.compose.ui.interop.UIKitInteropContext
import androidx.compose.ui.interop.UIKitInteropTransaction
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.input.PlatformTextInputService
import androidx.compose.ui.uikit.*
Expand Down Expand Up @@ -131,6 +133,7 @@ private class AttachedComposeContext(
view.bottomAnchor.constraintEqualToAnchor(parentView.bottomAnchor)
)
}

fun dispose() {
scene.close()
view.dispose()
Expand All @@ -146,9 +149,11 @@ internal actual class ComposeWindow : UIViewController {
private var isInsideSwiftUI = false
private val safeAreaState = mutableStateOf(IOSInsets())
private val layoutMarginsState = mutableStateOf(IOSInsets())
private val interopContext = UIKitInteropContext(requestRedraw = {
attachedComposeContext?.view?.needRedraw()
})
private val interopContext = UIKitInteropContext(
requestRedraw = {
attachedComposeContext?.view?.needRedraw()
}
)

/*
* Initial value is arbitarily chosen to avoid propagating invalid value logic
Expand Down Expand Up @@ -634,8 +639,8 @@ internal actual class ComposeWindow : UIViewController {
)
}

override fun retrieveCATransactionCommands(): List<() -> Unit> =
interopContext.getActionsAndClear()
override fun retrieveInteropTransaction(): UIKitInteropTransaction =
interopContext.retrieve()

override fun draw(surface: Surface, targetTimestamp: NSTimeInterval) {
// The calculation is split in two instead of
Expand All @@ -644,7 +649,8 @@ internal actual class ComposeWindow : UIViewController {
val integral = floor(targetTimestamp)
val fractional = targetTimestamp - integral
val secondsToNanos = 1_000_000_000L
val nanos = integral.roundToLong() * secondsToNanos + (fractional * 1e9).roundToLong()
val nanos =
integral.roundToLong() * secondsToNanos + (fractional * 1e9).roundToLong()

scene.render(surface.canvas, nanos)
}
Expand Down Expand Up @@ -679,26 +685,26 @@ internal actual class ComposeWindow : UIViewController {
}

private fun UIViewController.checkIfInsideSwiftUI(): Boolean {
var parent = parentViewController

while (parent != null) {
val isUIHostingController = parent.`class`()?.let {
val className = NSStringFromClass(it)
// SwiftUI UIHostingController has mangled name depending on generic instantiation type,
// It always contains UIHostingController substring though
return className.contains("UIHostingController")
} ?: false

if (isUIHostingController) {
return true
}

parent = parent.parentViewController
var parent = parentViewController

while (parent != null) {
val isUIHostingController = parent.`class`()?.let {
val className = NSStringFromClass(it)
// SwiftUI UIHostingController has mangled name depending on generic instantiation type,
// It always contains UIHostingController substring though
return className.contains("UIHostingController")
} ?: false

if (isUIHostingController) {
return true
}

return false
parent = parent.parentViewController
}

return false
}

private fun UIUserInterfaceStyle.asComposeSystemTheme(): SystemTheme {
return when (this) {
UIUserInterfaceStyle.UIUserInterfaceStyleLight -> SystemTheme.Light
Expand Down