Nicoară Talpeș

web and mobile developer

Requesting images async in Swift

This blog post will cover a source of silent bugs in Swift.

Problem outline

The goal is to populate a collection view with some local images from the iOS device. Start with the initial local identifiers of the images and feed them to fetchAssets. Then call asynchronously a method to obtain some pictures and put them in a dictionary.


var dict : Dictionary  = [:]
let lstAssets : PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: lstLocalIdentifiers, options: PHFetchOptions())
lstAssets.enumerateObjects({object , index, stop in
                
    self.phImageManager.requestImage(for: object, targetSize: CGSize(width: 150,height:150),
                                        contentMode: PHImageContentMode.aspectFill,
                                        options: self.phImageOptions,
                                        resultHandler: { (result : UIImage? , info) in
                                        if let res = result {                                                      
                                            dict[lstLocalIdentifiers[index]] = res
                                            }
                                        })

Here is the collection view showing the associated local identifiers:

The issue is that in fact, the images associated are wrong.

The correct association is :

For example, in the incorrect first collection , the second image with id "9CA.." corresponds to a girl in a white t-shirt, wereas in fact the id "9CA.." should correspond to a girl in a black t-shirt which is the last image in the correct second collection.

A couple of the images have the correct identifiers, and a couple do not. This is a silent bug that would normally bypass detection. How is it fixed?

Solution

An amazing feature that Xcode has is that hovering on "res" and clicking on the eye shows the picture. This helps in detecting the mismatches.

Digging deeper, notice the first result comes back in the callback after all the assets in lstLocalIdentifiers have requested the image. A second observation is that in the callback, printing the indexes of enumerateObjects show they come back incrementally: 0,1,... 8. But the index value does not correspond to the index of the object passed in enumerateObjects!

So it really seems that the closure cannot rely on the index of the enumerateObjects. This is the problematic method used:


open func enumerateObjects(_ block: (ObjectType, Int, UnsafeMutablePointer) -> Swift.Void)

Why does this matter? If a button was added to delete the multiple pictures that are selected, it would delete the wrong pictures based on other identifiers.

The solution is to request the images sequentially, for example with a recursive function:


var dict : Dictionary  = [:]
var lstLocalIdentifiers : [String] = []
func recursive(index : Int){
        
    if(index < lstLocalIdentifiers.count){
            
        var id : [String] = [lstLocalIdentifiers[index]]
            
        let lstAssets : PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: id, options: PHFetchOptions())
        lstAssets.enumerateObjects({object , index, stop in
                
            self.phImageManager.requestImage(for: object, targetSize: CGSize(width: 150,height:150),
                                                contentMode: PHImageContentMode.aspectFill,
                                                options: self.phImageOptions,
                                                resultHandler: { (result : UIImage? , info) in
                                                    
                                                if let res = result {
                                                    self.dict[self.lstLocalIdentifiers[index]] = res
                                                }
                                                self.recursive(index: index+1)
            })
        })
    }
    else{
        //reload collection view
    }
}

There are a few cases like this, where asynchronous calling does not have the safe behavior assumed. More to come!


Posted on April 7, 2017