Hi all! I'm rendering a UIKitViewController from c...
# compose-ios
m
Hi all! I'm rendering a UIKitViewController from compose multiplatform. This UIKitViewController is part of its own screen, which I open and close from compose multiplatform. However, its
deinit
isn't called every time I close the screen. It only happens sometimes when I close it for the second or third time, and it does so for older references. • I've added a log statement in a
DisposableEffect
, and it is indeed called when the composable is destroyed. • I've tried with a minimal example and this still happens • I'm on the latest version • When closing the screen, I use
rootNavController.popBackStack()
, which should remove all references. • I ran XCode's leak checker and there are no leaks. Is this a known bug?
Here's the minimal code:
Copy code
INFO: [LandmarkerCameraViewController2] bridgeCreateLandmarkerCamera called
INFO: [LandmarkerCameraViewController2] init called 1751962814.274656
INFO: [LandmarkerCameraViewController2] viewDidLoad 1751962814.274656
INFO: [LandmarkerCameraViewController2] willMove toParent: Optional(<ComposeHostingViewController: 0x157470400>)
INFO: [LandmarkerCameraViewController2] didMove toParent: Optional(<ComposeHostingViewController: 0x157470400>)
INFO: [LandmarkerCamera] DEBUG:: update called
INFO: [LandmarkerCameraViewController2] viewDidLayoutSubviews 1751962814.274656
INFO: [LandmarkerCameraViewController2] didMove toParent: Optional(<ComposeHostingViewController: 0x157470400>)
INFO: [LandmarkerCamera] DEBUG:: onDispose called!!!
INFO: [LandmarkerCameraViewController2] willMove toParent: nil
INFO: [LandmarkerCameraViewController2] viewWillDisappear
INFO: [LandmarkerCameraViewController2] didMove toParent: nil
INFO: [LandmarkerCamera] DEBUG:: onRelease called
INFO: [LandmarkerCameraViewController2] didMove toParent: nil
INFO: [LandmarkerCameraViewController2] bridgeCreateLandmarkerCamera called
INFO: [LandmarkerCameraViewController2] init called 1751962891.925012
INFO: [LandmarkerCameraViewController2] viewDidLoad 1751962891.925012
INFO: [LandmarkerCameraViewController2] willMove toParent: Optional(<ComposeHostingViewController: 0x157470400>)
INFO: [LandmarkerCameraViewController2] didMove toParent: Optional(<ComposeHostingViewController: 0x157470400>)
INFO: [LandmarkerCamera] DEBUG:: update called
INFO: [LandmarkerCameraViewController2] viewDidLayoutSubviews 1751962891.925012
INFO: [LandmarkerCameraViewController2] didMove toParent: Optional(<ComposeHostingViewController: 0x157470400>)
INFO: [LandmarkerCameraViewController2] deinit called 1751962814.274656
INFO: [LandmarkerCamera] DEBUG:: onDispose called!!!
INFO: [LandmarkerCameraViewController2] willMove toParent: nil
INFO: [LandmarkerCameraViewController2] viewWillDisappear
INFO: [LandmarkerCameraViewController2] didMove toParent: nil
INFO: [LandmarkerCamera] DEBUG:: onRelease called
INFO: [LandmarkerCameraViewController2] didMove toParent: nil
Copy code
class LandmarkerCameraViewController2: UIViewController {
    let key: TimeInterval
    
    public init() {
        self.key = Date().timeIntervalSince1970
        Log.shared.i(tag: "LandmarkerCameraViewController2", message: "init called \(key)")
        super.init(nibName: nil, bundle: nil)
    }
    
    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

    override func viewDidLoad() {
        Log.shared.i(tag: "LandmarkerCameraViewController2", message: "viewDidLoad \(key)")
        super.viewDidLoad()
    }

    override func viewDidLayoutSubviews() {
        Log.shared.i(tag: "LandmarkerCameraViewController2", message: "viewDidLayoutSubviews \(key)")
        super.viewDidLayoutSubviews()
    }
    
    override func willMove(toParent parent: UIViewController?) {
        super.willMove(toParent: parent)
        Log.shared.i(tag: "LandmarkerCameraViewController2", message: "willMove toParent: \(String(describing: parent))")
    }

    override func didMove(toParent parent: UIViewController?) {
        super.didMove(toParent: parent)
        Log.shared.i(tag: "LandmarkerCameraViewController2", message: "didMove toParent: \(String(describing: parent))")
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        Log.shared.i(tag: "LandmarkerCameraViewController2", message: "viewWillDisappear")
    }

    deinit {
        Log.shared.i(tag: "LandmarkerCameraViewController2", message: "deinit called \(key)")
    }
}
Copy code
Camera_iosKt.bridgeCreateLandmarkerCamera = {
        Log.shared.i(tag: "LandmarkerCameraViewController2", message: "bridgeCreateLandmarkerCamera called")
        return LandmarkerCameraViewController2()
    }
Copy code
lateinit var bridgeCreateLandmarkerCamera: () -> UIViewController
@Composable
internal actual fun LandmarkerCamera(
    modifier: Modifier,
    onLandmarkerResult: (LandmarkerResultCommon) -> Unit,
    onLandmarkerError: (String, LandmarkerErrorCommon) -> Unit,
) {
    UIKitViewController(
        factory = { bridgeCreateLandmarkerCamera() },
        modifier = Modifier.fillMaxSize(),
        update = {
            Log.i("LandmarkerCamera", "DEBUG:: update called")
        },
        onRelease = {
            Log.i("LandmarkerCamera", "DEBUG:: onRelease called")
        },
        onReset = {
            Log.i("LandmarkerCamera", "DEBUG:: onReset called")
        }
    )

    DisposableEffect(Unit) {
        onDispose {
            Log.i("LandmarkerCamera", "DEBUG:: onDispose called!!!")
        }
    }
}
a
Hi! Your
LandmarkerCameraViewController2
won't be deallocated instantly as the reference is captured by the garbage collector. However, the deallocation itself should happened eventually after the
onRelease
call - no guarantee it will be deallocated instantly. For debug purposes you can try calling
GC.collect()
several times, maybe with some short delay, after the
onRelease
call. If it won't help. feel free to file an issue in our YouTrack, attaching the link to the reproducer app.
m
@Andrei Salavei - thank you for the help! Relying on
GC.collect()
seems hacky and it doesn't work. My alternative is to simply not rely on
deinit
, but I don't know which option is best: • Do
val cameraController = remember { bridgeCreateLandmarkerCamera() }
instead so that I can call a cleanup function in
onRelease
• Call the function in
DisposableEffect
instead of
onRelease
• Call the cleanup function automatically on Swift when
willMoveToParent
is called with
null
• Trying to use SwiftUI instead of UIKit?
a
> Relying on
GC.collect()
seems hacky and it doesn't work. Sure it's not for production. I just need it to have a better understanding about the source of your issue. Did you try it? > My alternative is to simply not rely on
deinit
That's basically the solution I would recommend to go with. You can initialize your VC on
viewWillAppear
and cleanup on
viewDidDisappear
. All the solutions you're mentioned are not effect GC or composable and the way how it deallocates your interop view controller.
m
> Did you try it? Yes, but that didn't really help, unfortunately. Looks like relying on
viewDidAppear
and
viewDidDisappear
has fixed the issue? Thank you for the help 🙂
a
Yes, but that didn't really help, unfortunately.
I'll take a look there - that might be an issue.