SwiftUI UIIMagePickerController Find/Search Field Strange Behaviour

Viewed 33

I'm using a UIImagePickerController to select an image from the user's photo library. All works fine except when the user taps on the field with the magnifying glass icon to search the library. Sometimes it dismisses ImagePicker, other times it does nothing and occasionally it works as expected... any ideas?

Code to display the sheet:

Button(action: {}) {
  ZStack {
    Image(systemName: "photo")
      .foregroundColor(darkMode ? .white : .black)
  }
}
.frame(width: 44, height: 44)
.onTapGesture {
  showPhotoLibrary.toggle()
}
.sheet(isPresented: $showPhotoLibrary) {
  ImagePicker(sourceType: .photoLibrary, selectedImage: self.$image)
}

The ImagePicker:

import SwiftUI

struct ImagePicker: UIViewControllerRepresentable {
  
  var sourceType: UIImagePickerController.SourceType = .photoLibrary
  @Binding var selectedImage: UIImage
  @Environment(\.presentationMode) private var presentationMode
  
  func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
    
    let imagePicker = UIImagePickerController()

    imagePicker.sourceType = sourceType
    imagePicker.delegate = context.coordinator
    return imagePicker
  }
  
  func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
    
  }
  final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    var parent: ImagePicker
    init(_ parent: ImagePicker) {
      self.parent = parent
    }
    
    internal func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
      if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
        parent.selectedImage = image
      }
      parent.presentationMode.wrappedValue.dismiss()
    }
  }
  
  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
}
1 Answers

I've seen many struggle with this, it's all over the Apple dev forums so I thought I would have a go at solving it. From my own testing I believe the reason for the problems is UIImagePickerController is designed to be presented using the present method of a parent View Controller, not used as the root view controller of a SwiftUI presented sheet, as Apple (in their sample code) and everyone else seem to be doing. Below is my incomplete test code, which shows a working image picker where the search works and it can also be dragged down to dismiss, so I think this might be the correct solution.

The idea is we use UIViewControllerRepresentable to present our own custom view controller that presents or dismisses the image picker view controller when the present boolean changes.

struct ImagePickerTest: View {
    @State var show = false
    @State var image1: UIImage?
    
    var body: some View {
        VStack {
            Button("Hello, World!") {
                show.toggle()
            }
            Text("image: \(image1?.description ?? "" )")
        }
        .imagePicking(isPresented: $show, selectedImage: $image1)
    }
}


extension View {

    // this could be refactored into a `ViewModifier`
    func imagePicking(isPresented: Binding<Bool>, selectedImage: Binding<UIImage?>) -> some View {
        ZStack {
            ImagePicking(isPresented: isPresented, selectedImage: selectedImage)
            self
        }
    }
}
class MyViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate{
    
    var dismissPicker: (() -> Void)?
    var selectImage: ((UIImage) -> Void)?
    
    func showPickerIfNecessary() {
        if self.presentedViewController != nil {
            return
        }
        let imagePickerController = UIImagePickerController()
        imagePickerController.sourceType = .photoLibrary
        imagePickerController.delegate = self
        present(imagePickerController, animated: true)
    }
    
    func hidePickerIfNecessary() {
        if let vc = self.presentedViewController {
            vc.dismiss(animated: true)
        }
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        print("imagePickerControllerDidCancel")
        dismissPicker?()
    }
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
        print("didFinishPickingMediaWithInfo")
        if let image = (info[.editedImage] ?? info[.originalImage]) as? UIImage {
            selectImage?(image)
            dismissPicker?()
        }
    }
}

struct ImagePicking: UIViewControllerRepresentable {
    
    @Binding var isPresented: Bool
    @Binding var selectedImage: UIImage?
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        uiViewController.dismissPicker = {
            isPresented = false
        }
        uiViewController.selectImage = { image in
            selectedImage = image
        }
        
        if isPresented {
            uiViewController.showPickerIfNecessary()
        } else {
            uiViewController.hidePickerIfNecessary()
        }
    }
    
    func makeUIViewController(context: Context) -> some MyViewController {
        MyViewController()
    }
}

Before I started, I verified the OP's code was failing when typing a search, it crashed with this error:

2022-08-31 22:39:17.808899+0100 Test[66967:5425868] [UI] -[PUPhotoPickerHostViewController viewServiceDidTerminateWithError:] Error Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted}
Related