swiftuimemory-leaksuiimagepickercontrollerphpickerviewcontroller

Is this the proper way to use PHPickerViewController in SwiftUI? Because I'm getting a lot of leaks


I am trying to figure out if my code is causing the problem or if I should submit a bug report to Apple.

In a new project, I have this code:

ContentView()

import SwiftUI

struct ContentView: View {
    
    @State private var showingImagePicker = false
    @State private var inputImage: UIImage?
    @State private var image: Image?
    
    var body: some View {
        ZStack {
            Rectangle()
                .fill(Color.secondary)
            if image != nil {
                image?
                    .resizable()
                    .scaledToFit()
            } else {
                Text("Tap to select a picture")
                    .foregroundColor(.white)
                    .font(.headline)
            }
        }
        .onTapGesture {
            self.showingImagePicker = true
        }
        
        .sheet(isPresented: $showingImagePicker, onDismiss: loadImage){
            SystemImagePicker(image: self.$inputImage)
            
        }
    }
    func loadImage() {
        guard let inputImage = inputImage else { return }
        image = Image(uiImage: inputImage)
        
    }
}

SystemImagePicker.swift

import PhotosUI
import SwiftUI

struct SystemImagePicker: UIViewControllerRepresentable {
    
    @Environment(\.presentationMode) private var presentationMode
    
    @Binding var image: UIImage?
    
    func makeUIViewController(context: Context) -> PHPickerViewController {
        var configuration = PHPickerConfiguration()
        configuration.selectionLimit = 1
        configuration.filter = .images
        
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = context.coordinator
        return picker
    }
    
    func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
        
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(parent: self)
    }
    
    class Coordinator: NSObject, PHPickerViewControllerDelegate {
        let parent: SystemImagePicker
        
        init(parent: SystemImagePicker) {
            self.parent = parent
        }
        
        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            for img in results {
                guard img.itemProvider.canLoadObject(ofClass: UIImage.self) else { return }
                img.itemProvider.loadObject(ofClass: UIImage.self) { image, error in
                    if let error = error {
                        print(error)
                        return
                    }
                    
                    guard let image = image as? UIImage else { return }
                    self.parent.image = image
                    self.parent.presentationMode.wrappedValue.dismiss()
                }
            }
        }
    }
}

But when selecting just one image (as per my code, not selecting and then "changing my mind" and selecting another, different image), I get these leaks when running the memory graph in Xcode.

Memory graph leaks view

Is it my code, or is this on Apple?

For what it is worth, the Cancel button on the imagepicker doesn't work either. So, the user cannot just close the picker sheet, an image MUST be selected to dismiss the sheet.

Further note on old UIImagePickerController

Previously, I've used this code for the old UIImagePickerController

import SwiftUI

struct ImagePicker: UIViewControllerRepresentable {

    @Environment(\.presentationMode) var presentationMode
    @Binding var image: UIImage?
    
    class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
        let parent: ImagePicker
        
        init(_ parent: ImagePicker) {
           self.parent = parent
        }
        
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            if let uiImage = info[.originalImage] as? UIImage {
                parent.image = uiImage
            }
            parent.presentationMode.wrappedValue.dismiss()
        }
        
        deinit {
            print("deinit")
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.delegate = context.coordinator
        return picker
    }
    
    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
        
    }
}

This also result in leaks from choosing an image, but far fewer of them:

Old UIImagePickerController memory leaks


Solution

  • I know it's been over a year since you asked this question but hopefully this helps you or someone else looking for the answer.

    I used this code in a helper file:

    import SwiftUI
    import PhotosUI
    
    struct ImagePicker: UIViewControllerRepresentable {
    
    let configuration: PHPickerConfiguration
    @Binding var selectedImage: UIImage?
    @Binding var showImagePicker: Bool
    
    func makeCoordinator() -> Coordinator {
        
        Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> PHPickerViewController {
        
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = context.coordinator
        return picker
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        
        
    }
    }
    
    extension ImagePicker {
    
    class Coordinator: NSObject, PHPickerViewControllerDelegate {
        
        private let parent: ImagePicker
        
        init(_ parent: ImagePicker) {
            
            self.parent = parent
        }
        
        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            
            picker.dismiss(animated: true) {
                
                self.parent.showImagePicker = false
            }
            
            guard let provider = results.first?.itemProvider else { return }
            
            if provider.canLoadObject(ofClass: UIImage.self) {
                
                provider.loadObject(ofClass: UIImage.self) { image, _ in
                    
                    self.parent.selectedImage = image as? UIImage
                }
            }
            
            parent.showImagePicker = false
        }
    }
    }    
    

    This goes in your view (I set up configuration here so I could pass in custom versions depending on what I'm using the picker for, 2 are provided):

    @State private var showImagePicker = false
    @State private var selectedImage: UIImage?
    @State private var profileImage: Image?
    
    var profileConfig: PHPickerConfiguration {
        
        var config = PHPickerConfiguration()
        config.filter = .images
        config.selectionLimit = 1
        config.preferredAssetRepresentationMode = .current
        return config
    }
    
    var mediaConfig: PHPickerConfiguration {
        
        var config = PHPickerConfiguration()
        config.filter = .any(of: [.images, .videos])
        config.selectionLimit = 1
        config.preferredAssetRepresentationMode = .current
        return config
    }
    

    This goes in your body. You can customize it how you want but this is what I have so I didn't want to try and piece it out:

                    HStack {
                        
                        Button {
                            
                            showImagePicker.toggle()
                        } label: {
                            
                            Text("Select Photo")
                                .foregroundColor(Color("AccentColor"))
                        }
                        .sheet(isPresented: $showImagePicker) {
                            
                            loadImage()
                        } content: {
                            
                            ImagePicker(configuration: profileConfig, selectedImage: $selectedImage, showImagePicker: $showImagePicker)
                                
                        }
                    }
                    
                    if profileImage != nil {
                        
                        profileImage?
                            .resizable()
                            .scaledToFill()
                            .frame(width: 100, height: 100)
                            .clipShape(Circle())
                            .shadow(radius: 5)
                            .overlay(Circle().stroke(Color.black, lineWidth: 2))
                    }
                    else {
                        
                        Image(systemName: "person.crop.circle")
                            .resizable()
                            .foregroundColor(Color("AccentColor"))
                            .frame(width: 100, height: 100)
                    }
    

    I will also give you the func for loading the image (I will be resamp:

    func loadImage() {
        
        guard let selectedImage = selectedImage else { return }
        profileImage = Image(uiImage: selectedImage)        
    }
    

    I also used this on my Form to update the image if it is changed but you can use it on whatever you're using for your body (List, Form, etc. Whatever takes .onChange):

    .onChange(of: selectedImage) { _ in
                
                loadImage()
            }
    

    I noticed in a lot of tutorials there is little to no mention of this line which is what makes the cancel button function (I don't know if the closure is necessary but I added it and it worked so I left it in the example):

    picker.dismiss(animated: true)
    

    I hope I added everything to help you. It doesn't appear to leak anything and gives you use of the cancel button.

    Good luck!