Skip to content

Commit

Permalink
Initial implementation of iOS bindings to Lifecycle (#1043)
Browse files Browse the repository at this point in the history
Initial implementation of iOS bindings to Lifecycle.

<img width="836" alt="ios_lifecycle"
src="https://github.com/JetBrains/compose-multiplatform-core/assets/4167681/ec73ed26-5435-43e7-a4cf-5d3b9fe91693">
  • Loading branch information
elijah-semyonov authored and MatkovIvan committed Mar 16, 2024
1 parent a01964d commit 0b9a5c8
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 49 deletions.
33 changes: 33 additions & 0 deletions compose/mpp/demo/src/uikitMain/kotlin/NativePopupExample.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.mpp.demo.Screen
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
Expand All @@ -20,8 +25,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.interop.LocalUIViewController
import androidx.compose.ui.interop.UIKitView
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.ComposeUIViewController
import androidx.lifecycle.LifecycleEventObserver
import platform.UIKit.*
import platform.Foundation.*
import platform.darwin.dispatch_async
Expand Down Expand Up @@ -64,9 +71,35 @@ private fun NativeModalWithNavigation() {

@Composable
private fun NativeNavigationPage() {
val lifecycleOwner = LocalLifecycleOwner.current

val states = remember { mutableStateListOf<String>() }

val observer = remember {
LifecycleEventObserver { _, event ->
println(event)
states.add("${states.size} ${event.name}")
}
}

DisposableEffect(lifecycleOwner) {
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}

Column(Modifier.fillMaxSize().background(Color.DarkGray), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
val navigationController = LocalUIViewController.current.navigationController

LazyRow(Modifier.height(50.dp).fillMaxWidth()) {
items(states.size) { index ->
Box(Modifier.background(Color.White).padding(16.dp)) {
Text(states[index], color = Color.Black)
}
}
}

Button(onClick = {
navigationController?.pushViewController(
ComposeUIViewController {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2024 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.window

import kotlinx.cinterop.ObjCAction
import platform.Foundation.NSNotificationCenter
import platform.Foundation.NSSelectorFromString
import platform.UIKit.UIApplication
import platform.UIKit.UIApplicationDidEnterBackgroundNotification
import platform.UIKit.UIApplicationState
import platform.UIKit.UIApplicationWillEnterForegroundNotification
import platform.darwin.NSObject

internal class ApplicationStateListener(
/**
* Callback which will be called with `true` when the app becomes active, and `false` when the app goes background
*/
private val callback: (Boolean) -> Unit
) : NSObject() {
init {
val notificationCenter = NSNotificationCenter.defaultCenter

notificationCenter.addObserver(
this,
NSSelectorFromString(::applicationWillEnterForeground.name),
UIApplicationWillEnterForegroundNotification,
null
)

notificationCenter.addObserver(
this,
NSSelectorFromString(::applicationDidEnterBackground.name),
UIApplicationDidEnterBackgroundNotification,
null
)
}

@ObjCAction
fun applicationWillEnterForeground() {
callback(true)
}

@ObjCAction
fun applicationDidEnterBackground() {
callback(false)
}

/**
* Deregister from [NSNotificationCenter]
*/
fun dispose() {
val notificationCenter = NSNotificationCenter.defaultCenter

notificationCenter.removeObserver(this, UIApplicationWillEnterForegroundNotification, null)
notificationCenter.removeObserver(this, UIApplicationDidEnterBackgroundNotification, null)
}

companion object {
val isApplicationActive: Boolean
get() = UIApplication.sharedApplication.applicationState != UIApplicationState.UIApplicationStateBackground
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.LocalSystemTheme
import androidx.compose.ui.SystemTheme
import androidx.compose.ui.interop.LocalUIViewController
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.PlatformContext
import androidx.compose.ui.platform.PlatformWindowContext
import androidx.compose.ui.scene.ComposeScene
Expand Down Expand Up @@ -92,6 +93,8 @@ internal class ComposeContainer(
private val configuration: ComposeUIViewControllerConfiguration,
private val content: @Composable () -> Unit,
) : CMPViewController(nibName = null, bundle = null) {
val lifecycleOwner = ViewControllerBasedLifecycleOwner()

private var isInsideSwiftUI = false
private var mediator: ComposeSceneMediator? = null
private val layers: MutableList<UIViewComposeSceneLayer> = mutableListOf()
Expand Down Expand Up @@ -234,6 +237,8 @@ internal class ComposeContainer(

isInsideSwiftUI = checkIfInsideSwiftUI()
createMediatorIfNeeded()

lifecycleOwner.handleViewWillAppear()
configuration.delegate.viewWillAppear(animated)
}

Expand Down Expand Up @@ -263,6 +268,7 @@ internal class ComposeContainer(
kotlin.native.internal.GC.collect()
}

lifecycleOwner.handleViewDidDisappear()
configuration.delegate.viewDidDisappear(animated)
}

Expand Down Expand Up @@ -337,6 +343,7 @@ internal class ComposeContainer(
}

private fun dispose() {
lifecycleOwner.dispose()
mediator?.dispose()
mediator = null
layers.fastForEach {
Expand Down Expand Up @@ -420,6 +427,7 @@ internal fun ProvideContainerCompositionLocals(
LocalUIViewController provides this,
LocalInterfaceOrientation provides interfaceOrientationState.value,
LocalSystemTheme provides systemThemeState.value,
LocalLifecycleOwner provides lifecycleOwner,
content = content
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,11 @@ import androidx.compose.ui.util.trace
import kotlin.math.roundToInt
import kotlinx.cinterop.*
import org.jetbrains.skia.*
import platform.Foundation.NSNotificationCenter
import platform.Foundation.NSRunLoop
import platform.Foundation.NSSelectorFromString
import platform.Foundation.NSThread
import platform.Metal.MTLCommandBufferProtocol
import platform.QuartzCore.*
import platform.UIKit.UIApplicationDidEnterBackgroundNotification
import platform.UIKit.UIApplicationWillEnterForegroundNotification
import platform.darwin.*
import org.jetbrains.skia.Rect
import platform.Foundation.NSLock
Expand Down Expand Up @@ -109,51 +106,6 @@ private class DisplayLinkConditions(
}
}

private class ApplicationStateListener(
/**
* Callback which will be called with `true` when the app becomes active, and `false` when the app goes background
*/
private val callback: (Boolean) -> Unit
) : NSObject() {
init {
val notificationCenter = NSNotificationCenter.defaultCenter

notificationCenter.addObserver(
this,
NSSelectorFromString(::applicationWillEnterForeground.name),
UIApplicationWillEnterForegroundNotification,
null
)

notificationCenter.addObserver(
this,
NSSelectorFromString(::applicationDidEnterBackground.name),
UIApplicationDidEnterBackgroundNotification,
null
)
}

@ObjCAction
fun applicationWillEnterForeground() {
callback(true)
}

@ObjCAction
fun applicationDidEnterBackground() {
callback(false)
}

/**
* Deregister from [NSNotificationCenter]
*/
fun dispose() {
val notificationCenter = NSNotificationCenter.defaultCenter

notificationCenter.removeObserver(this, UIApplicationWillEnterForegroundNotification, null)
notificationCenter.removeObserver(this, UIApplicationDidEnterBackgroundNotification, null)
}
}

internal interface MetalRedrawerCallbacks {
/**
* Perform time step and encode draw operations into canvas.
Expand Down Expand Up @@ -292,7 +244,7 @@ internal class MetalRedrawer(
// and won't receive UIApplicationWillEnterForegroundNotification
// so we compare the state with UIApplicationStateBackground instead of UIApplicationStateActive
displayLinkConditions.isApplicationActive =
UIApplication.sharedApplication.applicationState != UIApplicationState.UIApplicationStateBackground
ApplicationStateListener.isApplicationActive

caDisplayLink.addToRunLoop(NSRunLoop.mainRunLoop, NSRunLoopCommonModes)

Expand Down
Loading

0 comments on commit 0b9a5c8

Please sign in to comment.