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
    }
}
J. Doe
  • 12,159
  • 9
  • 60
  • 114
  • @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`. – J. Doe Mar 30 '22 at 14:03
  • This seems similar to this https://stackoverflow.com/questions/56699009/swiftui-memory-leak-when-using-uiviewcontrollerrepresentable, and this https://developer.apple.com/forums/thread/118582 but with no solution yet. – Christos Koninis Apr 01 '22 at 09:53
  • @ChristosKoninis it works for UIViewController, but not for UIImagePickerController – NoKey Apr 01 '22 at 12:03

2 Answers2

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
        )
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 1
    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? – NoKey Apr 03 '22 at 11:51
  • UIViewControllerRepresentable it's never release I'm testing with iOS 16.1.1 with Xcode 14.1 14B47b – Cristian Cardoso Jul 13 '23 at 02:41
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.

Cristik
  • 30,989
  • 25
  • 91
  • 127