iosswiftphpickerviewcontroller

PHPickerViewController Disregards Order Photo is Selected


I use a PHPickerViewController to select profile images and want the first photo selected to be a user's hero image.

Whenever I return the selected results, the order in which I selected is not reflected.

I've double checked my config and looked at existing SO answers, but no solution has worked and would appreciate any guidance that does not involve using a 3rd party!

Apple's documentation states by simply setting the .selection property to .ordered, a user's selected order should be respected, but it does not...

//Setup code

var config = PHPickerConfiguration()    
config.selectionLimit = 3
    config.filter = .images
    config.selection = .ordered
    
    let picker = PHPickerViewController(configuration: config)
    picker.delegate = self

//Delegate handler

func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
                
guard !results.isEmpty else {
    picker.dismiss(animated: true)
    return
}

self.photos = []

var tempImages: [Int: UIImage] = [:]
let dispatchGroup = DispatchGroup()

for (index, result) in results.enumerated() {
     
     dispatchGroup.enter() // Enter the group

     result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] object, error in
         defer { dispatchGroup.leave() }

         guard let self = self else { return }

         if let image = object as? UIImage {
             
             tempImages[index] = image
         }
     }
 }

dispatchGroup.notify(queue: .main) { [weak self] in
                            
    guard let self = self else { return }
    
    for index in 0..<tempImages.keys.count {
        
        if let image = tempImages[index] {
                                
            self.photos?.append(image)
        }
    }
}
        
picker.dismiss(animated: true)
}

Solution

  • This won't answer the question of the order in which the results array arrives, but you can greatly simplify your code and be certain of getting your images in the same order as the results array by introducing this extension to NSItemProvider:

    extension NSItemProvider {
        @objc func loadImage() async throws -> UIImage {
            enum ImageLoadingError: Error {
                case couldNotConvertDataToImage
                case unknown
            }
            return try await withCheckedThrowingContinuation { continuation in
                self.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, error in
                    if let data {
                        if let image = UIImage(data: data) {
                            continuation.resume(returning: image)
                        } else {
                            continuation.resume(throwing: ImageLoadingError.couldNotConvertDataToImage)
                        }
                    } else {
                        continuation.resume(throwing: error ?? ImageLoadingError.unknown)
                    }
                }
            }
        }
    }
    

    This will allow you to simply loop in a very ordinary way thru the results, in order, without all the hair-raising complication of the dispatch group:

    var images = [UIImage]()
    for result in results {
        let provider = result.itemProvider
        do {
            let image = try await provider.loadImage()
            images.append(image)
        } catch {
            print(error.localizedDescription)
        }
    }
    

    (You'll have to get into an async context to make that call, obviously; but that's easy.) The output images is guaranteed to match the order of the results and the code is far simpler and can be reasoned about.