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] Application restarts when open a Camera #4025

Closed
yandroidUA opened this issue Dec 10, 2023 · 3 comments
Closed

[iOS] Application restarts when open a Camera #4025

yandroidUA opened this issue Dec 10, 2023 · 3 comments
Labels
bug Something isn't working duplicate This issue or pull request already exists ios

Comments

@yandroidUA
Copy link

yandroidUA commented Dec 10, 2023

Describe the bug
When the user opens a camera through the UIImagePickerController and then cancels or makes a photo the application "reloads" or losses its state. Not sure whether it's a bug of Compose or not, I'm using a following function to obtain an instance of my router:

@Composable
fun <D> rememberNavigationState(
    mapper: ((D) -> KClass<out ViewModel>?)? = null,
    initialConfiguration: TransitionScope.(NavigationState<D>) -> Unit = {},
    initialDestination: D,
    flowFinalizer: FlowFinalizer,
): NavigationState<D> {
    val manager = LocalViewModelManager.current
    return remember {
        NavigationState(
            initialDestination = initialDestination,
            initialConfiguration = initialConfiguration,
            viewModelExtension = mapper?.let { ViewModelExtension(manager = manager, mapping = it) },
            flowFinalizer = flowFinalizer
        )
    }
}

So, from my understanding it should not be reinstantiated.
From the logs I see that ComposeUIViewController invokes children composables one the application become active after the camera and with the function above my NavigationState get reinstantiated and I don't know why.

Implementation to open the Camera & Gallery
// TODO need to store somewhere globally, otherwise iOS wipes it out and these are never called back
private var imagePickerCoordinator: ImagePickerCoordinator? = null
private var imagePickerController: UIImagePickerController? = null

@Composable
internal actual fun ImagePickerResultHandler(
    source: ImageSource?,
    onRequestSourceDialog: () -> Unit,
    onShowPermissionRationale: (Permission) -> Unit,
    onLoading: (Boolean) -> Unit,
    onError: (String) -> Unit,
    onResult: (RuntimeImage) -> Unit
) {
    val fileManager = LocalFileManager.current
    val currentController = UIApplication.sharedApplication.keyWindow?.rootViewController ?: LocalUIViewController.current

    imagePickerCoordinator = remember {
        ImagePickerCoordinator(onImagePicked = { onResult(RuntimeImage(it)) }, onPickerDismissed = onRequestSourceDialog)
    }
    imagePickerController = remember(imagePickerCoordinator) {
        UIImagePickerController().apply {
            delegate = imagePickerCoordinator
        }
    }

    LaunchedEffect(source) {
        when (source) {
            ImageSource.Camera -> {
                Napier.d(tag = "IMAGE_PICKER") { "Camera" }
                imagePickerController?.sourceType = UIImagePickerControllerSourceType.UIImagePickerControllerSourceTypeCamera
                imagePickerController?.cameraCaptureMode = UIImagePickerControllerCameraCaptureMode.UIImagePickerControllerCameraCaptureModePhoto
                currentController.showViewController(imagePickerController!!, null)
            }

            ImageSource.Gallery -> {
                imagePickerController?.sourceType = UIImagePickerControllerSourceType.UIImagePickerControllerSourceTypePhotoLibrary
                currentController.showViewController(imagePickerController!!, null)
            }

            is ImageSource.RemoteUrl -> {
                fileManager.getImageFromCacheOrDownload(source.url, force = true)
                    .collect { state ->
                        when (state) {
                            is ApiState.Failed -> onError(state.reason)
                            ApiState.Idle -> {}
                            ApiState.Loading -> onLoading(true)
                            is ApiState.Succeed -> state.data.representation?.image?.let(onResult) ?: onError("")
                        }
                    }
            }
            null -> Unit
        }
    }
}

private class ImagePickerCoordinator(
    private val onImagePicked: (ByteArray) -> Unit,
    private val onPickerDismissed: () -> Unit
) : NSObject(), UIImagePickerControllerDelegateProtocol, UINavigationControllerDelegateProtocol {

    override fun imagePickerController(
        picker: UIImagePickerController,
        didFinishPickingImage: UIImage,
        editingInfo: Map<Any?, *>?
    ) {
        Napier.d(tag = "IMAGE_PICKER") { "imagePickerController" }
        picker.dismissViewControllerAnimated(true, null)
        val imageBytes = UIImagePNGRepresentation(didFinishPickingImage)?.toByteArray()
        imageBytes?.run(onImagePicked)
    }

    override fun imagePickerControllerDidCancel(picker: UIImagePickerController) {
        Napier.d(tag = "IMAGE_PICKER") { "imagePickerControllerDidCancel" }
        picker.dismissViewControllerAnimated(true, null)
        onPickerDismissed()
    }

}

Affected platforms
Select one of the platforms below:

  • iOS

Versions

  • Kotlin version*: 1.9.20
  • Compose Multiplatform version*: 1.5.10
  • OS version(s)* (required for Desktop and iOS issues): iOS 15
  • OS architecture (x86 or arm64): arm64

To Reproduce

Expected behavior
Since I'm using remember I'm expecting to retrieve a same instance of the router.

Screenshots

Screen.Recording.2023-12-10.at.1.54.28.PM.mov

Additional context
Obviously, it works fine if I save the NavigationState globally, but should I do that ? From my understanding, it's something that remember is responsible, doesn't it ?

@yandroidUA yandroidUA added bug Something isn't working submitted labels Dec 10, 2023
@m-sasha
Copy link
Contributor

m-sasha commented Dec 11, 2023

Hi, thanks for the report. Could you provide a short but complete reproducer of the problem?

@m-sasha m-sasha added wait for reply Further information is requested ios and removed submitted labels Dec 11, 2023
@yandroidUA
Copy link
Author

yandroidUA commented Dec 11, 2023

Sure, here is a sample project - archive.

XCode: Version 15.0 (15A240d)

P.S. Please, ignore the GoogleSignIn pod in the app, initially it was a reproducer for the other issue, but it doesn't impact this issue

Screen.Recording.2023-12-11.at.10.16.02.AM.mov

From the logs:

CONTENT
STATE: com.app.test.NavigationState@1b958020
STATE: com.app.test.NavigationState@1b958020
IMAGE_PICKER: Camera
IMAGE_PICKER: imagePickerControllerDidCancel
CONTENT
STATE: com.app.test.NavigationState@1b95ca20

Same behaviour on the real iOS device


I'm not sure whether it's a Compose problem, but in case it is not I'd be apriciate to receive any advice how I can handle this case

@terrakok
Copy link
Collaborator

@yandroidUA it is the known issue with a compose scene lifecycle: #3890

I will close this as a duplicate

@terrakok terrakok added duplicate This issue or pull request already exists and removed wait for reply Further information is requested labels Dec 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working duplicate This issue or pull request already exists ios
Projects
None yet
Development

No branches or pull requests

3 participants