3

I'm trying to use the UiKit API PHPickerViewController using KMM and Compose for iOS.

import androidx.compose.runtime.Composable
import androidx.compose.ui.interop.LocalUIViewController
import platform.PhotosUI.PHPickerConfiguration
import platform.PhotosUI.PHPickerViewController
import platform.PhotosUI.PHPickerViewControllerDelegateProtocol
import platform.darwin.NSObject

@Composable
actual fun pickerController() {
    val uiViewController = LocalUIViewController.current
    val configuration = PHPickerConfiguration()
    val pickerController = PHPickerViewController(configuration)
    val pickerDelegate = object : NSObject(), PHPickerViewControllerDelegateProtocol {
        override fun picker(picker: PHPickerViewController, didFinishPicking: List<*>) {
            println("didFinishPicking: $didFinishPicking")
            picker.dismissViewControllerAnimated(flag = false, completion = {})
            uiViewController.dismissModalViewControllerAnimated(false)
        }
    }

    pickerController.setDelegate(pickerDelegate)
    uiViewController.presentViewController(pickerController, animated = false, completion = null)
}

This displays the image picker:

Unfortunately, when clicking on Cancel, the delegate callback is not called, and I get the following message on the console:

[Picker] PHPickerViewControllerDelegate doesn't respond to picker:didFinishPicking:

Is it possible to implement the callback in Kotlin?
What am I missing?

Nico
  • 2,570
  • 1
  • 9
  • 17
  • there's a chance `pickerDelegate` is released - `setDelegate` doesn't increase reference counter of delegate object, in Swift this object would've been released right after exiting `pickerController` function, not sure how it works with kotlin memory model, but I would've stored it somehow – Phil Dukhov Jun 03 '23 at 00:43
  • Thank you for the hint @PhilDukhov! I tried to persist the delegate in a global property but unfortunately it doesn't solve the issue... – Nico Jun 03 '23 at 07:19

1 Answers1

2

Since pickerDelegate is NSObject, it's lifecycle follows ObjC rules, not KMM memory model.

So as soon as the execution leaves composable block, this objects gets released - as setDelegate takes it as weak reference.

You can fix it by storing it using remember.

Also using your function is dangerous because you're gonna call presentViewController on each recomposition - e.g. if some of your reactive data changes on the calling side.

You can update it to return an action that will present it, but store delegate and the action itself using remember:

@Composable
actual fun rememberOpenPickerAction(): () -> Unit {
    val uiViewController = LocalUIViewController.current
    val pickerDelegate = remember {
        object : NSObject(), PHPickerViewControllerDelegateProtocol {
            override fun picker(picker: PHPickerViewController, didFinishPicking: List<*>) {
                println("didFinishPicking: $didFinishPicking")
                picker.dismissViewControllerAnimated(flag = false, completion = {})
            }
        }
    }

    return remember {
        {
            val configuration = PHPickerConfiguration()
            val pickerController = PHPickerViewController(configuration)
            pickerController.setDelegate(pickerDelegate)
            uiViewController.presentViewController(pickerController, animated = true, completion = null)
        }
    }
}

Usage:

Button(onClick = rememberOpenPickerAction()) {

}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220