Fogmeister
Fogmeister

Reputation: 77631

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.

Upvotes: 11

Views: 2756

Answers (4)

F22lightning
F22lightning

Reputation: 640

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))
          }
        }
      }
    }
  }
}

Upvotes: 8

Frustrated_Student
Frustrated_Student

Reputation: 175

I started getting a weird UI bug after the PHPickerViewController crashed where the keyboard was not visible but my views were still being squashed. So I suspected a keyboard / avoidance issue. I disabled keyboard avoidance in a parent view and managed to stop it from crashing.

.ignoresSafeArea(.keyboard)

Upvotes: 3

JeanNicolas
JeanNicolas

Reputation: 437

.... still a iOS bug in 15.0. I've modified Fogmeister's class Coordinator to return the image in addition to the PHPickerResult.

struct WrappedPickerView: UIViewControllerRepresentable {
  @Environment(\.presentationMode) var presentationMode
  @Binding var photoPickerResult: PHPickerResult?
  @Binding var image: UIImage?

  let wrappedPicker = WrappedPhotoPicker()

  func makeUIViewController(context: Context) -> WrappedPhotoPicker {
       var config = PHPickerConfiguration()
       config.filter = .images
       config.selectionLimit = 1

       let picker = PHPickerViewController(configuration: config)
       picker.delegate = context.coordinator

       wrappedPicker.picker = picker
       return wrappedPicker
   }

   func updateUIViewController(_ uiViewController: WrappedPhotoPicker, context: Context) {}

   func makeCoordinator() -> Coordinator {
    Coordinator(self)
   }

  class Coordinator: PHPickerViewControllerDelegate {
      let parent: WrappedPickerView

      init(_ parent: WrappedPickerView) {
          self.parent = parent
      }

     func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        self.parent.presentationMode.wrappedValue.dismiss()
        self.parent.wrappedPicker.dismiss(animated: false)
  
        self.parent.photoPickerResult = results.first
        print(results)
    
        guard let result = results.first else {
        return
       }
    
    
       self.parent.image = nil
    
       DispatchQueue.global().async {
            result.itemProvider.loadObject(ofClass: UIImage.self) { (object, error) in

         guard let imageLoaded = object as? UIImage else {
                return
            }
            DispatchQueue.main.async {
                self.parent.image = imageLoaded
            }    
         }
     }
    
    
   }
  }
 }

Upvotes: 2

Fogmeister
Fogmeister

Reputation: 77631

Well.. this seems to be an iOS bug.

I have cerated a sample project here that shows the bug... https://github.com/oliverfoggin/BrokenImagePickers

And a replica project here written with UIKit that does not... https://github.com/oliverfoggin/UIKit-Image-Pickers

I tried to take a screen recording of this happening but it appears that if any screen recording is happening (whether on device or via QuickTime on the Mac) this suppresses the bug from happening.

I have filed a radar with Apple and sent them both projects to have a look at and LOTS of detail around what's happening. I'll keep this updated with any progress on that.

Hacky workaround

After a bit of further investigation I found that you can start with SwiftUI and then present a PHPickerViewController without this crash happening.

From SwiftUI if you present a UIViewControllerRepresentable... and then from there if you present the PHPickerViewController it will not crash.

So I came up with a (very tacky) workaround that avoids this crash.

I first create a UIViewController subclass that I use like a wrapper.

class WrappedPhotoPicker: UIViewController {
  var picker: PHPickerViewController?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    if let picker = picker {
      present(picker, animated: false)
    }
  }
}

Then in the SwiftUI View I create this wrapper and set the picker in it.

struct WrappedPickerView: UIViewControllerRepresentable {
  @Environment(\.presentationMode) var presentationMode
  @Binding var photoPickerResult: PHPickerResult?
  
  let wrappedPicker = WrappedPhotoPicker()
  
  func makeUIViewController(context: Context) -> WrappedPhotoPicker {
    var config = PHPickerConfiguration()
    config.filter = .images
    config.selectionLimit = 1
    
    let picker = PHPickerViewController(configuration: config)
    picker.delegate = context.coordinator
    
    wrappedPicker.picker = picker
    return wrappedPicker
  }
  
  func updateUIViewController(_ uiViewController: WrappedPhotoPicker, context: Context) {}
  
  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
  
  class Coordinator: PHPickerViewControllerDelegate {
    let parent: WrappedPickerView
    
    init(_ parent: WrappedPickerView) {
      self.parent = parent
    }
    
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
      parent.presentationMode.wrappedValue.dismiss()
      parent.wrappedPicker.dismiss(animated: false)
      
      parent.photoPickerResult = results.first
    }
  }
}

This is far from ideal as I'm presenting at the wrong time and stuff. But it works until Apple provide a permanent fix for this.

Upvotes: 5

Related Questions