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

4

11

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.

Hoicks answered 23/9, 2021 at 19:51 Comment(13)
+1 Also experiencing this issue. I’m displaying the Image picker within a fullScreenCover using SwiftUI. The picker functions as expected until the search box is focused and then I also get viewServiceDidTerminateWithError.Cerebral
Related: developer.apple.com/forums/thread/690802 and developer.apple.com/forums/thread/687588?answerId=688948022Cerebral
Yup, that’s my forum post. 😃Hoicks
It looks like a bug so far as I can see, I’ve even gone down the thought path of seeing if there is a new permission required to perform a photo search. What’s frustrating is there doesn’t appear to be an easy way to hide the search bar which means pushing with a known bug 😫Cerebral
@Cerebral I just tried taking a screen recording of the sample app to show the bug. Found out that if you’re doing a screen recording the bug doesn’t occur. It only happens when not screen recording. 😂Hoicks
Just tried this - can confirm! 🤦🏻‍♂️Cerebral
I’ve just terminated my Photos app and now I can’t reproduce!?Cerebral
Yes, mine started working after I did the screen recording. But then restarting the app it is breaking again now.Hoicks
Any joy if you close the Photos app?Cerebral
I don’t have the photos app open. Sometimes I need to hit the search bar and scroll a long way before it crashes.Hoicks
Let us continue this discussion in chat.Cerebral
Same problem, seems to be a bug?!Amine
fix for viewServiceDidTerminateWithError https://mcmap.net/q/1015936/-swiftui-uiimagepickercontroller-find-search-field-strange-behaviourSolfeggio
J
8

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))
          }
        }
      }
    }
  }
}
Jape answered 10/2, 2022 at 17:27 Comment(3)
This is definitely the correct answer (while a workaround is still needed!) I had also tried the wrapped view approach, with some success but it had some edge cases where it didn't seem to work for me (when called further down the stack). Using ignoresSafeArea on the ImagePicker in the view works well.Bloodworth
We are experiencing something similar on iPad (but not in the simulator) using PHPickerViewController. The first attempt to display the gallery is successfull but then the next attempt results in the "Unable to load Items <Try Again>" screen. This pattern repeats over and over again. Our project is written in Objective-c. Is this solution and workaround valid for objective-c projects ?Critique
@Critique this is intended for SwiftUI so unless you are using SwiftUI this should not work.Jape
H
5

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.

Hoicks answered 25/9, 2021 at 10:0 Comment(4)
Was there any update to this Radar? The issue seems to persist in iOS 15.1. Yet to test 15.2.Costar
@Costar no, no update yet.Hoicks
Thanks. Now tested on iOS 15.2 and this issue still persists. Hopefully this gets resolved before the next major release.Costar
@Costar if this is anything like the previous image picker bug that went for 7 major versions of iOS before it was fixed. And it was only “fixed” because they replaced UIImagePickerController with PHPickerViewController so essentially rewrote it from the ground up.Hoicks
I
3

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)
Impacted answered 2/1, 2022 at 12:55 Comment(0)
A
2

.... 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
            }    
         }
     }
    
    
   }
  }
 }
Amine answered 28/11, 2021 at 10:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.