1

The deinit block inside Custom is not called. I also tried the onDismiss variant instead of the isPresent Binding, but both do not run the deinit block for type Custom.

To reproduce my problem either clone the app and run it, or check out the code below. The deinit block is called when directly subclassing UIViewController, but it goes wrong for UIImagePickerController.

Clone: https://github.com/Jasperav/MemoryLeak

Code:

import Combine
import SwiftUI

struct ContentView: View {
    @State var present = false

    var body: some View {
        Button("click me") {
            present = true
        }
        .sheet(isPresented: $present) {
            MediaPickerViewWrapperTest(isPresented: $present)
        }
    }
}

class Custom: UIImagePickerController {
    deinit {
        print("DEINIT")
    }
}

struct MediaPickerViewWrapperTest: UIViewControllerRepresentable {
    let isPresented: Binding<Bool>

    func makeUIViewController(context: Context) -> Custom {
        let c = Custom()

        c.delegate = context.coordinator

        return c
    }

    func updateUIViewController(_: Custom, context _: Context) {}

    func makeCoordinator() -> Coordinator {
        Coordinator(
            isPresented: isPresented
        )
    }
}

final class Coordinator: NSObject, UINavigationControllerDelegate,
    UIImagePickerControllerDelegate
{
    @Binding var isPresented: Bool

    init(
        isPresented: Binding<Bool>
    ) {
        _isPresented = isPresented
    }

    func imagePickerController(
        _: UIImagePickerController,
        didFinishPickingMediaWithInfo _: [
            UIImagePickerController
                .InfoKey: Any
        ]
    ) {
        isPresented = false
    }

    func imagePickerControllerDidCancel(_: UIImagePickerController) {
        isPresented = false
    }
}
3
  • @Asperi That's not true. Did you even bother trying out the code? When setting up a breakpoint inside the sheet closure, you will see it is reached only when you click on the button. Furthermore I already stated the deinit block is called when using a UIViewController, so I expect the same thing for UIImagePickerController. Commented Mar 30, 2022 at 14:03
  • This seems similar to this stackoverflow.com/questions/56699009/…, and this developer.apple.com/forums/thread/118582 but with no solution yet. Commented Apr 1, 2022 at 9:53
  • @ChristosKoninis it works for UIViewController, but not for UIImagePickerController Commented Apr 1, 2022 at 12:03

2 Answers 2

0

A possible workaround is to use view representable instead.

Tested with Xcode 13.2 / iOS 15.2

demo

Below is modified part only, everything else is the same:

struct MediaPickerViewWrapperTest: UIViewRepresentable {
    let isPresented: Binding<Bool>

    func makeUIView(context: Context) -> UIView {
        let c = Custom()

        c.delegate = context.coordinator

        return c.view
    }

    func updateUIView(_: UIView, context _: Context) {}

    func makeCoordinator() -> Coordinator {
        Coordinator(
            isPresented: isPresented
        )
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

I use a @StateObject in the View which presents the MediaPickerViewWrapperTest. After the MediaPickerViewWrapperTest is dismissed, either by clicking cancel or picking media, and I dismiss the presenting View, the StateObject is never deinitialized. The StateObject is deinitialized when the MediaPickerViewWrapperTest is never presented. You know why?
UIViewControllerRepresentable it's never release I'm testing with iOS 16.1.1 with Xcode 14.1 14B47b
0

When do de-initializers run? When the reference of the object reaches zero.

Thus, you expect the reference count to become zero when the picker is dismissed or is removed from the UI hierarchy. And while that might well happen, it's not guaranteed to.

Reference counting is not that simple, especially once you've handed your object to another framework (UIKit in this case). Once you do it, you no longer have full control over the lifecycle of that object. The internal implementation details of the other framework can keep the object alive more than you assume it would, thus the deinit code might not be called with the timing you expect.

Recommeding to instead rely on UIViewController's didMove(toParent:) method, and write the cleanup logic there.

And even if you're not handing your custom class instance to another framework, relying on the object's lifecycle for other side effects is not always reliable, as the object can end up being retained by unexpected new owners.

Bottom line - deinit should be used to clean up stuff related to that particular object.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.