Comments (12)
Hi @DevSrSouza,
I tried your workaround but it's not working also.
But I found a brilliant solution by luck while looking for some library and is working fine for me
//
// AvoidDisposeViewController.swift
// iosApp
//
// Created by Clem on 27.10.22.
// Copyright © 2022 orgName. All rights reserved.
//
import UIKit
import SwiftUI
struct AvoidDisposeViewControllerRepresentable: UIViewControllerRepresentable {
let composeController: UIViewController
func makeUIViewController(context: Context) -> UIViewController {
return AvoidDisposeViewController(composeController)
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
class AvoidDisposeViewController: UIViewController {
let controller: UIViewController
init(_ controller: UIViewController) {
self.controller = controller
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
addChild(controller)
view.addSubview(controller.view)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
controller.removeFromParent()
controller.view.removeFromSuperview()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
skiaRefresh()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
skiaRefresh()
}
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
skiaRefresh()
}
private func skiaRefresh() {
controller.view.frame = view.bounds
controller.viewWillAppear(false)
controller.view.touchesCancelled([UITouch()], with: UIEvent())
}
}
from voyager.
@adrielcafe @DevSrSouza any help here?
from voyager.
Can you share how you are displaying the Compose content? I mean, I want the main app Screen that you are adding the ComposeUiViewController to see if I can reproduce here.
If possible, you can share a app containing this issue that I can debug it? Maybe trying to reproduce in the sample app of Voyager and comment here the changes.
from voyager.
Hi @DevSrSouza,
There is no significant or different implementation I'm using. If you have any sample or any running app just add a functionality to open native camera UIViewController for example and then return back to our Compose UIViewController and then you will find it reloaded.
from voyager.
This is a very quick sample app with the issue,
Just add this file in your iosMain module and render the generated MainViewController in your native iOS app code to see the issue.
Also this is a loom recording with the issue description:
(https://www.loom.com/share/8ce50514742a459396583d347e0d99b7)
Thanks in advance :)
package com.example.app
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.interop.LocalUIViewController
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.ComposeUIViewController
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import platform.UIKit.UIImagePickerController
import platform.UIKit.UIImagePickerControllerSourceType
import platform.UIKit.UIViewController
/**
* ================MainViewController================
*/
fun MainViewController() = ComposeUIViewController {
// Then render app
Navigator(
screen = LandingScreen
)
}
/**
* ================Landing Screen================
*/
internal object LandingScreen : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.current
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(
space = 2.dp,
alignment = Alignment.CenterVertically
)
) {
Text(
text = "Welcome to Sample App",
color = Color.Black,
fontSize = 20.sp
)
Text(
text = "Navigate to Create Profile",
color = Color.White,
fontSize = 20.sp,
modifier = Modifier
.background(Color.Blue)
.padding(12.dp)
.clickable {
navigator?.push(
CreateProfileScreen
)
}
)
}
}
}
/**
* ================Create Profile Screen================
*/
internal object CreateProfileScreen : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.current
val rootViewController = LocalUIViewController.current
val imagePicker = remember {
ImagePicker(
rootController = rootViewController
)
}
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(
space = 2.dp,
alignment = Alignment.CenterVertically
)
) {
Text(
text = "Click to Go Back",
color = Color.White,
fontSize = 20.sp,
modifier = Modifier
.background(Color.Blue)
.padding(12.dp)
.clickable {
navigator?.pop()
}
)
Text(
text = "Open Camera",
color = Color.White,
fontSize = 20.sp,
modifier = Modifier
.background(Color.Blue)
.padding(12.dp)
.clickable {
imagePicker.openCamera()
}
)
}
}
}
/**
* ================Image Picker================
*/
internal class ImagePicker(
private val rootController: UIViewController
) {
private val pickerController = UIImagePickerController().apply {
sourceType = UIImagePickerControllerSourceType.UIImagePickerControllerSourceTypeCamera
}
fun openCamera() {
rootController.presentViewController(pickerController, true, null)
}
}
from voyager.
I have being validating your sample, thanks for sharing btw.
It seems that Compose iOS complete destroy the composition when you do a presentViewController
.
Voyager relies 100% in the rememberSaveable
API.
Any simple rememberSaveable
is not being restored in this case.
val randomNumber = rememberSaveable {
Random.nextInt()
}
Text(text = "$randomNumber")
from voyager.
I have open a issue in Compose Multiplatform with your snippet.
JetBrains/compose-multiplatform#4112
from voyager.
@DevSrSouza Thanks Gabriel, hope they fix it soon
from voyager.
I have found a workaround for now.
object ComposeStateRestoration {
var valuesToBeRestored: MutableState<Map<String, List<Any?>>> = mutableStateOf(emptyMap())
}
fun MainViewController() = ComposeUIViewController {
CompositionLocalProvider(
LocalSaveableStateRegistry provides SaveableStateRegistry(
ComposeStateRestoration.valuesToBeRestored.value,
canBeSaved = { true }
)
) {
// ... your app source code here, navigator, etc
// last item in the code block
val registry = LocalSaveableStateRegistry.current
DisposableEffect(Unit) {
onDispose {
val savedStates = registry?.performSave()
ComposeStateRestoration.valuesToBeRestored.value = savedStates ?: emptyMap()
}
}
}
}
This solution is only for "single view controller" Compose iOS apps. If you have multiples ViewControllers and use Compose together with UiKit/SwiftUI you can change the ComposeStateRestoration
to a class
and add it as a parameter in the function that exposes the ComposeUiViewController (in this example MainViewController
) and then, hold in the memory a instance of the ComposeStateRestoration
.
from voyager.
And here is a patch validating the solution on Voyager Multiplatform sample.
diff --git a/samples/multiplatform/src/iosMain/kotlin/MainViewController.kt b/samples/multiplatform/src/iosMain/kotlin/MainViewController.kt
index dee0b39..48d1463 100644
--- a/samples/multiplatform/src/iosMain/kotlin/MainViewController.kt
+++ b/samples/multiplatform/src/iosMain/kotlin/MainViewController.kt
@@ -1,4 +1,156 @@
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
+import androidx.compose.runtime.saveable.SaveableStateRegistry
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.interop.LocalUIViewController
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.ComposeUIViewController
-import cafe.adriel.voyager.sample.multiplatform.SampleApplication
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.screen.ScreenKey
+import cafe.adriel.voyager.core.screen.uniqueScreenKey
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.Navigator
+import platform.UIKit.UIImagePickerController
+import platform.UIKit.UIImagePickerControllerSourceType
+import platform.UIKit.UIViewController
-fun MainViewController() = ComposeUIViewController { SampleApplication() }
+object ComposeStateRestoration {
+ var valuesToBeRestored: MutableState<Map<String, List<Any?>>> = mutableStateOf(emptyMap())
+}
+
+fun MainViewController() = ComposeUIViewController {
+ CompositionLocalProvider(
+ LocalSaveableStateRegistry provides SaveableStateRegistry(
+ ComposeStateRestoration.valuesToBeRestored.value,
+ canBeSaved = { true }
+ )
+ ) {
+ val registry = LocalSaveableStateRegistry.current
+
+ Navigator(LandingScreen)
+
+ DisposableEffect(Unit) {
+ onDispose {
+ val savedStates = registry?.performSave()
+ ComposeStateRestoration.valuesToBeRestored.value = savedStates ?: emptyMap()
+ }
+ }
+ }
+}
+
+internal data object LandingScreen : Screen {
+ override val key: ScreenKey = uniqueScreenKey
+
+ @Composable
+ override fun Content() {
+ val navigator = LocalNavigator.current
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(
+ space = 2.dp,
+ alignment = Alignment.CenterVertically
+ )
+ ) {
+ Text(
+ text = "Welcome to Sample App",
+ color = Color.Black,
+ fontSize = 20.sp
+ )
+
+ Text(
+ text = "Navigate to Create Profile",
+ color = Color.White,
+ fontSize = 20.sp,
+ modifier = Modifier
+ .background(Color.Blue)
+ .padding(12.dp)
+ .clickable {
+ navigator?.push(
+ CreateProfileScreen
+ )
+ }
+ )
+ }
+ }
+}
+
+internal data object CreateProfileScreen : Screen {
+ override val key: ScreenKey = uniqueScreenKey
+
+ @Composable
+ override fun Content() {
+ val navigator = LocalNavigator.current
+ val rootViewController = LocalUIViewController.current
+ val imagePicker = remember {
+ ImagePicker(
+ rootController = rootViewController
+ )
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(
+ space = 2.dp,
+ alignment = Alignment.CenterVertically
+ )
+ ) {
+ Text(
+ text = "Click to Go Back",
+ color = Color.White,
+ fontSize = 20.sp,
+ modifier = Modifier
+ .background(Color.Blue)
+ .padding(12.dp)
+ .clickable {
+ navigator?.pop()
+ }
+ )
+
+ Text(
+ text = "Open Camera",
+ color = Color.White,
+ fontSize = 20.sp,
+ modifier = Modifier
+ .background(Color.Blue)
+ .padding(12.dp)
+ .clickable {
+ imagePicker.openCamera()
+ }
+ )
+ }
+ }
+}
+
+internal class ImagePicker(
+ private val rootController: UIViewController
+) {
+ private val pickerController = UIImagePickerController().apply {
+ sourceType = UIImagePickerControllerSourceType.UIImagePickerControllerSourceTypeCamera
+ }
+
+ fun openCamera() {
+ rootController.presentViewController(pickerController, true, null)
+ }
+}
Steps the apply the code:
- Create a file
state_restauration_workaround.patch
with the above patch - run
git apply state_restauration_workaround.patch
from voyager.
This is not a Voyager issue, this workaround will do the work.
I will keep this issue open until we have proper documentation.
from voyager.
I think it should be fixed on Compose KMP 1.6.10-beta01
from voyager.
Related Issues (20)
- Layout Inspector and Live Edit are not working HOT 1
- Screens are not cleared when calling navigator.replaceAll HOT 4
- Tab recreate
- Retain old screen HOT 3
- Nested navigators inside Tabs don't get disposed when the TabNavigator gets disposed.
- push Screen Crash with Double BottomSheetNavigator and Transition
- Screen as popup or transparent packground HOT 2
- remember loses state after navigation HOT 9
- Koin-Integration: getting scoped ScreenModel
- Support for Shared Element Transition
- Make it possible to pass Scaffold PaddingValues to CurrentTab() HOT 3
- RegisterScreen:transition was used multiple times HOT 10
- java.lang.IllegalArgumentException: Key xxx:transition was used multiple times on kotlin 2.0.0-RC2 & r8 HOT 3
- voyager-koin causes build error for iOS Simulator HOT 2
- What is the right way to have different Windowinsets for every screen HOT 2
- Dispatchers.Main in androidUnitTest kmm HOT 4
- Crashing in bottom navigation
- Nested Screen in Bottom Navigation
- Voyager 1.1.0-beta01 issues
- navigator will close automatically
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from voyager.