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

Fix iOS lifecycle #660

Closed
wants to merge 6 commits into from
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Please refer to [2.0.0-alpha10 – Migration guide](2.0.0-alpha10.md)
- [#654](https://github.com/bumble-tech/appyx/pull/654) - Renamings
- [#657](https://github.com/bumble-tech/appyx/pull/657) - Rename ParentNode & Node to Node and LeafNode
- [#644](https://github.com/bumble-tech/appyx/pull/644) – Refactor AppyxComponent and application of draggable modifier
- [#660](https://github.com/bumble-tech/appyx/pull/660) - Make IosNodeHost to receive a Lifecycle class to fix iOS lifecycle problems
Copy link
Contributor

Choose a reason for hiding this comment

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

Better to surround code related names with `

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's move this to pending changes, alpha10 is already released


### Fixed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.bumble.appyx.navigation.lifecycle.Lifecycle
import com.bumble.appyx.navigation.node.Node
import com.bumble.appyx.navigation.platform.LocalOnBackPressedDispatcherOwner
import com.bumble.appyx.navigation.platform.OnBackPressedDispatcher
import com.bumble.appyx.navigation.platform.OnBackPressedDispatcherOwner
import com.bumble.appyx.navigation.platform.PlatformLifecycleRegistry
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectory
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
import kotlinx.cinterop.ExperimentalForeignApi
Expand All @@ -27,12 +27,10 @@ fun <N : Node<*>> IosNodeHost(
onBackPressedEvents: Flow<Unit>,
modifier: Modifier = Modifier,
integrationPoint: IntegrationPoint,
lifecycle: Lifecycle,
customisations: NodeCustomisationDirectory = remember { NodeCustomisationDirectoryImpl(null) },
factory: NodeFactory<N>,
) {
val platformLifecycleRegistry = remember {
PlatformLifecycleRegistry()
}
val mainScreen = UIScreen.mainScreen
val screenBounds = mainScreen.bounds

Expand All @@ -58,7 +56,7 @@ fun <N : Node<*>> IosNodeHost(

CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides onBackPressedDispatcherOwner) {
NodeHost(
lifecycle = platformLifecycleRegistry,
lifecycle = lifecycle,
integrationPoint = integrationPoint,
modifier = modifier,
customisations = customisations,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.bumble.appyx.navigation.platform

import com.bumble.appyx.navigation.lifecycle.Lifecycle

class LifecycleHelper {
val lifecycle: PlatformLifecycleRegistry = PlatformLifecycleRegistry()

fun created() {
lifecycle.setCurrentState(Lifecycle.State.CREATED)
}

fun resumed() {
lifecycle.setCurrentState(Lifecycle.State.RESUMED)
}

fun started() {
lifecycle.setCurrentState(Lifecycle.State.STARTED)
}

fun destroyed() {
lifecycle.setCurrentState(Lifecycle.State.DESTROYED)
}
}
2 changes: 2 additions & 0 deletions demos/appyx-navigation/ios/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ kotlin {
framework {
baseName = "ios"
isStatic = true
export(project(":appyx-navigation:appyx-navigation"))
}
license = "Apache License, Version 2.0"
authors = "https://github.com/bumble-tech/"
Expand All @@ -36,6 +37,7 @@ kotlin {
iosSimulatorArm64Main.dependsOn(this)
dependencies {
implementation(project(":demos:appyx-navigation:common"))
api(project(":appyx-navigation:appyx-navigation"))
api(compose.runtime)
api(compose.foundation)
api(compose.material)
Expand Down
6 changes: 4 additions & 2 deletions demos/appyx-navigation/ios/src/iosMain/kotlin/main.ios.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import com.bumble.appyx.demos.navigation.node.root.RootNode
import com.bumble.appyx.demos.navigation.ui.AppyxSampleAppTheme
import com.bumble.appyx.navigation.integration.IosNodeHost
import com.bumble.appyx.navigation.integration.MainIntegrationPoint
import com.bumble.appyx.navigation.platform.LifecycleHelper
import kotlinx.coroutines.flow.flowOf
import platform.Foundation.NSURL

private val integrationPoint = MainIntegrationPoint()
private val navigator = Navigator()

@Suppress("FunctionNaming")
fun MainViewController() = ComposeUIViewController {
fun MainViewController(lifecycleHelper: LifecycleHelper) = ComposeUIViewController {
AppyxSampleAppTheme {
Scaffold(
modifier = Modifier
Expand All @@ -33,7 +34,8 @@ fun MainViewController() = ComposeUIViewController {
IosNodeHost(
modifier = Modifier,
onBackPressedEvents = flowOf(),
integrationPoint = integrationPoint
integrationPoint = integrationPoint,
lifecycle = lifecycleHelper.lifecycle,
) { nodeContext ->
RootNode(
nodeContext = nodeContext,
Expand Down
9 changes: 7 additions & 2 deletions demos/appyx-navigation/iosApp/iosApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@ import SwiftUI
import ios

struct ComposeView: UIViewControllerRepresentable {

var lifecycleHelper: LifecycleHelper

func makeUIViewController(context: Context) -> UIViewController {
Main_iosKt.MainViewController()
Main_iosKt.MainViewController(lifecycleHelper: lifecycleHelper)
}

func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

struct ContentView: View {
var lifecycleHelper: LifecycleHelper

var body: some View {
ComposeView()
ComposeView(lifecycleHelper: lifecycleHelper)
.ignoresSafeArea(.all)
}
}
37 changes: 36 additions & 1 deletion demos/appyx-navigation/iosApp/iosApp/iOSApp.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,52 @@
import SwiftUI
import ios
import Foundation
import UIKit

@main
struct iOSApp: App {

@UIApplicationDelegateAdaptor(AppDelegate.self)
var appDelegate: AppDelegate

@Environment(\.scenePhase) var scenePhase

var body: some Scene {
WindowGroup {
ZStack {
Color.white.ignoresSafeArea(.all) // status bar color
ContentView()
ContentView(lifecycleHelper: appDelegate.lifecycleHolder.lifecycleHelper)
}
.onOpenURL { incomingURL in
Main_iosKt.handleDeepLinks(url: incomingURL)
}
.onChange(of: scenePhase) { newPhase in
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: fix indentation

switch newPhase {
case .background: appDelegate.lifecycleHolder.lifecycleHelper.created()
case .inactive: appDelegate.lifecycleHolder.lifecycleHelper.created()
case .active: appDelegate.lifecycleHolder.lifecycleHelper.resumed()
@unknown default: break
}
zsoltk marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}

class AppDelegate: NSObject, UIApplicationDelegate {
let lifecycleHolder: LifecycleHolder = LifecycleHolder()
}

class LifecycleHolder {

let lifecycleHelper: LifecycleHelper

init() {
lifecycleHelper = LifecycleHelper()
lifecycleHelper.created()
}

deinit {
// Destroy the root component before it is deallocated
lifecycleHelper.destroyed()
}
}
2 changes: 2 additions & 0 deletions demos/sandbox-appyx-navigation/ios/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ kotlin {
framework {
baseName = "ios"
isStatic = true
export(project(":appyx-navigation:appyx-navigation"))
}
}

Expand All @@ -34,6 +35,7 @@ kotlin {
iosSimulatorArm64Main.dependsOn(this)
dependencies {
implementation(project(":demos:sandbox-appyx-navigation:common"))
api(project(":appyx-navigation:appyx-navigation"))
api(compose.runtime)
api(compose.foundation)
api(compose.material)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.bumble.appyx.demos.sandbox.navigation.node.container.MainNavNode
import com.bumble.appyx.demos.sandbox.navigation.ui.AppyxSampleAppTheme
import com.bumble.appyx.navigation.integration.IosNodeHost
import com.bumble.appyx.navigation.integration.MainIntegrationPoint
import com.bumble.appyx.navigation.platform.LifecycleHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
Expand All @@ -29,7 +30,7 @@ val backEvents: Channel<Unit> = Channel()
private val integrationPoint = MainIntegrationPoint()

@Suppress("FunctionNaming")
fun MainViewController() = ComposeUIViewController {
fun MainViewController(lifecycleHelper: LifecycleHelper) = ComposeUIViewController {

AppyxSampleAppTheme {
val coroutineScope = rememberCoroutineScope()
Expand All @@ -45,7 +46,8 @@ fun MainViewController() = ComposeUIViewController {
IosNodeHost(
modifier = Modifier,
onBackPressedEvents = backEvents.receiveAsFlow(),
integrationPoint = remember { integrationPoint }
lifecycle = lifecycleHelper.lifecycle,
integrationPoint = remember { integrationPoint },
) { nodeContext ->
MainNavNode(
nodeContext = nodeContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@ import SwiftUI
import ios

struct ComposeView: UIViewControllerRepresentable {

var lifecycleHelper: LifecycleHelper

func makeUIViewController(context: Context) -> UIViewController {
Main_iosKt.MainViewController()
Main_iosKt.MainViewController(lifecycleHelper: lifecycleHelper)
}

func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

struct ContentView: View {
var lifecycleHelper: LifecycleHelper

var body: some View {
ComposeView()
ComposeView(lifecycleHelper: lifecycleHelper)
.ignoresSafeArea(.all)
}
}
43 changes: 40 additions & 3 deletions demos/sandbox-appyx-navigation/iosApp/iosApp/iOSApp.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,50 @@
import SwiftUI
import ios
import Foundation
import UIKit

@main
struct iOSApp: App {

@UIApplicationDelegateAdaptor(AppDelegate.self)
var appDelegate: AppDelegate

@Environment(\.scenePhase) var scenePhase

var body: some Scene {
WindowGroup {
ZStack {
Color.white.ignoresSafeArea(.all) // status bar color
ContentView()
}.preferredColorScheme(.light)
ContentView(lifecycleHelper: appDelegate.lifecycleHolder.lifecycleHelper)
}
.preferredColorScheme(.light)
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .background: appDelegate.lifecycleHolder.lifecycleHelper.created()
case .inactive: appDelegate.lifecycleHolder.lifecycleHelper.created()
case .active: appDelegate.lifecycleHolder.lifecycleHelper.resumed()
@unknown default: break
}
}
}
Comment on lines +21 to 29
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: fix indentation

}
}
}

class AppDelegate: NSObject, UIApplicationDelegate {
let lifecycleHolder: LifecycleHolder = LifecycleHolder()
}

class LifecycleHolder {

let lifecycleHelper: LifecycleHelper

init() {
lifecycleHelper = LifecycleHelper()
lifecycleHelper.created()
}

deinit {
// Destroy the root component before it is deallocated
lifecycleHelper.destroyed()
}
}
7 changes: 5 additions & 2 deletions documentation/navigation/multiplatform.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,18 @@ fun main() {

### iOS

For a complete example on how to pass a `LifecycleHelper` class to `MainViewController` please refer to our [multiplatform starter kit](https://github.com/bumble-tech/appyx-starter-kit/tree/a7331be581a6727597eab35744fe1bcd26f3fa87/iosApp/iosApp)

```kotlin
val backEvents: Channel<Unit> = Channel()

fun MainViewController() = ComposeUIViewController {
fun MainViewController(lifecycleHelper: LifecycleHelper) = ComposeUIViewController {
YourAppTheme {
IosNodeHost(
modifier = Modifier,
lifecycle = lifecycleHelper.lifecycle,
// See back handling section in the docs below!
onBackPressedEvents = backEvents.receiveAsFlow()
onBackPressedEvents = backEvents.receiveAsFlow(),
) {
RootNode(
nodeContext = it
Expand Down