iosswiftuiphpickerviewcontroller

PHPickerViewController tapping on Search gets error... "Unable to load photos"


I'm trying to implement a PHPickerViewController using SwiftUI and The Composable Architecture. (Not that I think that's particularly relevant but it might explain why some of my code is like it is).

Sample project

I've been playing around with this to try and work it out. I created a little sample Project on GitHub which removes The Composable Architecture and keeps the UI super simple.

https://github.com/oliverfoggin/BrokenImagePickers/tree/main

It looks like iOS 15 is breaking on both the UIImagePickerViewController and the PHPickerViewController. (Which makes sense as they both use the same UI under the hood).

I guess the nest step is to determine if the same error occurs when using them in a UIKit app.

My code

My code is fairly straight forward. It's pretty much just a reimplementation of the same feature that uses UIImagePickerViewController but I wanted to try with the newer APIs.

My code looks like this...

public struct ImagePicker: UIViewControllerRepresentable {

// Vars and setup stuff...
  @Environment(\.presentationMode) var presentationMode

  let viewStore: ViewStore<ImagePickerState, ImagePickerAction>
  
  public init(store: Store<ImagePickerState, ImagePickerAction>) {
    self.viewStore = ViewStore(store)
  }
  
// UIViewControllerRepresentable required functions
  public func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> some UIViewController {

    // Configuring the PHPickerViewController
    var config = PHPickerConfiguration()
    config.filter = PHPickerFilter.images
    
    let picker = PHPickerViewController(configuration: config)
    picker.delegate = context.coordinator
    return picker
  }
  
  public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
  
  public func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
  
// This is the coordinator that acts as the delegate
  public class Coordinator: PHPickerViewControllerDelegate {
    let parent: ImagePicker
    
    init(_ parent: ImagePicker) {
      self.parent = parent
    }
    
    public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
      picker.dismiss(animated: true)
      
      guard let itemProvider = results.first?.itemProvider,
        itemProvider.canLoadObject(ofClass: UIImage.self) else {
        return
      }
      
      itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
        if let image = image as? UIImage {
          DispatchQueue.main.async {
            self?.parent.viewStore.send(.imagePicked(image: image))
          }
        }
      }
    }
  }
}

All this works in the simple case

I can present the ImagePicker view and select a photo and it's all fine. I can cancel out of it ok. I can even scroll down the huge collection view of images that I have. I can even see the new image appear in my state object and display it within my app. (Note... this is still WIP and so the code is a bit clunky but that's only to get it working initially).

The problem case

The problem is that when I tap on the search bar in the PHPickerView (which is a search bar provided by Apple in the control, I didn't create it or code it). It seems to start to slide up the keyboard and then the view goes blank with a single message in the middle...

Unable to Load Photos

[Try Again]

I also get a strange looking error log. (I removed the time stamps to shorten the lines).

// These happen on immediately presenting the ImagePicker
AppName[587:30596] [Picker] Showing picker unavailable UI (reason: still loading) with error: (null)
AppName[587:30596] Writing analzed variants.


// These happen when tapping the search bar
AppName[587:30867] [lifecycle] [u A95D90FC-C77B-43CC-8FC6-C8E7C81DD22A:m (null)] [com.apple.mobileslideshow.photospicker(1.0)] Connection to plugin interrupted while in use.
AppName[587:31002] [lifecycle] [u A95D90FC-C77B-43CC-8FC6-C8E7C81DD22A:m (null)] [com.apple.mobileslideshow.photospicker(1.0)] Connection to plugin invalidated while in use.
AppName[587:30596] [Picker] Showing picker unavailable UI (reason: crashed) with error: (null)
AppName[587:30596] viewServiceDidTerminateWithError:: Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted}

Tapping the "Try Again" button reloads the initial scroll screen and I can carry on using it. But tapping the search bar again just shows the same error.

I'm usually the first one to point out that the error is almost definitely not with the Apple APIs but I'm stumped on this one. I'm not sure what it is that I'm doing that is causing this to happen?

Is it the fact that it's in a SwiftUI view?

Recreated the project in UIKit

I remade the same project using UIKit... https://github.com/oliverfoggin/UIKit-Image-Pickers

And I couldn't replicate the crash at all.

Also... if you are taking any sort of screen recording of the device the crash will not happen. I tried taking a recording on the device itself and couldn't replicate it. I also tried doing a movie recording from my Mac using the iPhone screen and couldn't replicate the crash. But... the instant I stopped the recording on QuickTime the crash was replicable again.


Solution

  • This fixed it for me .ignoreSafeArea(.keyboard) like @Frustrated_Student mentions.

    To elaborate on @Frustrated_Student this issue has to do with the UIViewControllerRepresentable treating the view like many SwiftUI views to automatically avoid the keyboard. If you are presenting the picker using a sheet as I am then you can simply add the .ignoreSafeArea(.keyboard) to the UIViewControllerRepresentable view in my case I called it ImagePicker here is a better example.

    Where to add it the .ignoreSafeArea(.keyboard)

    .sheet(isPresented: $imagePicker) {
        ImagePicker(store: store)
            .ignoresSafeArea(.keyboard)
    }
    

    This is @Fogmeister code:

    public struct ImagePicker: UIViewControllerRepresentable {
    
    // Vars and setup stuff...
      @Environment(\.presentationMode) var presentationMode
    
      let viewStore: ViewStore<ImagePickerState, ImagePickerAction>
      
      public init(store: Store<ImagePickerState, ImagePickerAction>) {
        self.viewStore = ViewStore(store)
      }
      
    // UIViewControllerRepresentable required functions
      public func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> some UIViewController {
    
        // Configuring the PHPickerViewController
        var config = PHPickerConfiguration()
        config.filter = PHPickerFilter.images
        
        let picker = PHPickerViewController(configuration: config)
        picker.delegate = context.coordinator
        return picker
      }
      
      public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
      
      public func makeCoordinator() -> Coordinator {
        Coordinator(self)
      }
      
    // This is the coordinator that acts as the delegate
      public class Coordinator: PHPickerViewControllerDelegate {
        let parent: ImagePicker
        
        init(_ parent: ImagePicker) {
          self.parent = parent
        }
        
        public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
          picker.dismiss(animated: true)
          
          guard let itemProvider = results.first?.itemProvider,
            itemProvider.canLoadObject(ofClass: UIImage.self) else {
            return
          }
          
          itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
            if let image = image as? UIImage {
              DispatchQueue.main.async {
                self?.parent.viewStore.send(.imagePicked(image: image))
              }
            }
          }
        }
      }
    }