iosMain.io.github.alexzhirkevich.cupertino.decompose.UIKitChildren.kt Maven / Gradle / Ivy
/*
* Copyright (c) 2023-2024. Compose Cupertino project and open source contributors.
*
* 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.
*
*/
@file: Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
package io.github.alexzhirkevich.cupertino.decompose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalContext
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.currentCompositionLocalContext
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.SaveableStateHolder
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.ui.Modifier
import androidx.compose.ui.interop.LocalUIViewController
import androidx.compose.ui.interop.UIKitView
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.uikit.OnFocusBehavior
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.window.ComposeUIViewController
import com.arkivanov.decompose.Child
import com.arkivanov.decompose.InternalDecomposeApi
import com.arkivanov.decompose.hashString
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.value.Value
import com.arkivanov.essenty.backhandler.BackDispatcher
import io.github.alexzhirkevich.cupertino.InternalCupertinoApi
import io.github.alexzhirkevich.cupertino.SystemBarAppearance
import io.github.alexzhirkevich.cupertino.rememberCupertinoHapticFeedback
import io.github.alexzhirkevich.cupertino.theme.CupertinoTheme
import io.github.alexzhirkevich.cupertino.theme.isInitializedCupertinoTheme
import kotlinx.cinterop.ExperimentalForeignApi
import platform.UIKit.UIGestureRecognizer
import platform.UIKit.UIGestureRecognizerDelegateProtocol
import platform.UIKit.UINavigationController
import platform.UIKit.UIView
import platform.UIKit.UIViewController
import platform.UIKit.addChildViewController
import platform.UIKit.didMoveToParentViewController
import platform.UIKit.removeFromParentViewController
import platform.UIKit.willMoveToParentViewController
@Composable
fun UIKitChildren(
stack: Value>,
modifier: Modifier = Modifier,
backDispatcher: BackDispatcher,
content: @Composable (child: Child.Created) -> Unit,
) {
val compositionLocalContext = rememberUpdatedState(currentCompositionLocalContext)
val stateHolder = rememberSaveableStateHolder()
val navController = remember {
NavController(
compositionLocalContext = compositionLocalContext,
stateHolder = stateHolder,
stack = stack,
backDispatcher = backDispatcher,
content = content,
)
}
navController.Content(modifier)
DisposableEffect(navController){
onDispose {
navController.release()
}
}
}
private class UIViewControllerWrapper(
val item : Child.Created,
private val backDispatcher: BackDispatcher,
private val compositionLocalContext: State,
private val content: @Composable () -> Unit,
) : UIViewController(null,null), UIGestureRecognizerDelegateProtocol {
@OptIn(ExperimentalForeignApi::class, InternalCupertinoApi::class)
override fun loadView() {
super.loadView()
val controller = ComposeUIViewController(
configure = {
onFocusBehavior = OnFocusBehavior.DoNothing
}
) {
val foundationContext = currentCompositionLocalContext
CompositionLocalProvider(
compositionLocalContext.value,
) {
CompositionLocalProvider(
context = foundationContext,
){
if (isInitializedCupertinoTheme()) {
SystemBarAppearance(CupertinoTheme.colorScheme.isDark, this)
}
CompositionLocalProvider(
LocalHapticFeedback provides rememberCupertinoHapticFeedback(),
LocalUIViewController provides this,
content = content
)
}
}
}
controller.willMoveToParentViewController(this)
controller.view.setFrame(view.frame)
view.addSubview(controller.view)
addChildViewController(controller)
controller.didMoveToParentViewController(this)
}
@OptIn(ExperimentalForeignApi::class)
override fun viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
this.view.subviews.forEach {
it as UIView
it.setFrame(view.frame)
}
}
override fun viewDidDisappear(animated: Boolean) {
super.viewDidDisappear(animated)
if (isMovingFromParentViewController()) {
backDispatcher.back()
}
}
}
private class NavController(
private val compositionLocalContext: State,
private val stateHolder: SaveableStateHolder,
private val stack : Value>,
private val backDispatcher: BackDispatcher,
private val content: @Composable (child: Child.Created) -> Unit,
) : UINavigationController(nibName = null, bundle = null), UIGestureRecognizerDelegateProtocol {
init {
navigationBarHidden = true
}
private val cancellation = stack.observe {
onChanged(it)
}
@OptIn(ExperimentalForeignApi::class)
@Composable
fun Content(modifier: Modifier) {
stateHolder.retainStates(stack.value.getConfigurations())
val parent = LocalUIViewController.current
DisposableEffect(parent){
willMoveToParentViewController(parent)
parent.addChildViewController(this@NavController)
didMoveToParentViewController(parent)
onDispose {
removeFromParentViewController()
}
}
UIKitView(
modifier = modifier,
factory = {
view
},
background = CupertinoTheme.colorScheme.systemBackground
)
}
override fun viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = this
}
override fun gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer): Boolean {
return backDispatcher.isEnabled && viewControllers.size > 1
}
fun release() {
cancellation.cancel()
}
private fun onChanged(stack: ChildStack) {
val controllers = viewControllers.filterIsInstance>()
val newControllers = stack.items.map {
controllers.fastFirstOrNull { c -> c.item.instance === it.instance }
?: makeUIViewController(it)
}
setViewControllers(newControllers, animated = true)
}
@OptIn(InternalDecomposeApi::class)
private fun makeUIViewController(
item: Child.Created
) = UIViewControllerWrapper(
backDispatcher = backDispatcher,
compositionLocalContext = compositionLocalContext,
content = {
stateHolder.SaveableStateProvider(item.configuration.hashString()) {
content(item)
}
},
item = item,
)
}
@OptIn(InternalDecomposeApi::class)
private fun ChildStack<*, *>.getConfigurations(): Set =
items.mapTo(HashSet()) { it.configuration.hashString() }
@Composable
private fun SaveableStateHolder.retainStates(currentKeys: Set) {
val keys = remember(this) { Keys(currentKeys) }
DisposableEffect(this, currentKeys) {
keys.set.forEach {
if (it !in currentKeys) {
removeState(it)
}
}
keys.set = currentKeys
onDispose {}
}
}
private class Keys(
var set: Set
)
© 2015 - 2025 Weber Informatics LLC | Privacy Policy