Becky Hansmeyer Becky Hansmeyer - 1 month ago 24
iOS Question

Maintaining order when requesting AVAssets

I am attempting to build a video merging app that allows users to select several short clips from a collection view and then generates a preview of the videos all merged into one. I am using the Photos framework (PHCachingImageManager) to populate the collection view and am passing an array of the selected PHAssets to the function below in order to request low quality AVAssets (for merging & generating the preview).

The problem is, I need to keep the AVAssets in the order in which the user selected them, but the "requestAVAsset" function is asynchronous and the completion handler is often called multiple times. I've never used Dispatch Groups before, but attempted to use them below...and the AVAssets are still out of order sometimes.

func requestAVAssets(assets: [PHAsset]) -> [AVAsset] {
var videoArray: [AVAsset] = []
let dispatchGroup = DispatchGroup()
let videoOptions = PHVideoRequestOptions()
videoOptions.isNetworkAccessAllowed = true
videoOptions.deliveryMode = .fastFormat
for asset in assets {
dispatchGroup.enter()
self.imageManager.requestAVAsset(forVideo: asset, options: videoOptions, resultHandler: { (video, audioMix, info) in
guard video != nil else { return }
videoArray.append(video!)
dispatchGroup.leave()
})
}
dispatchGroup.wait()
return videoArray
}


I'm guessing I've either misplaced some code or am approaching this in entirely the wrong way! Any suggestions are appreciated.

Answer Source

If you capture the current index while you're iterating the AVAssets, you can insert rather than append. That's how I do it, at least.

func requestAVAssets(assets: [PHAsset]) -> [AVAsset] {
    var videoArray = [AVAsset?](repeating: nil, count: assets.count)
    let videoOptions = PHVideoRequestOptions()
    videoOptions.isNetworkAccessAllowed = true
    videoOptions.deliveryMode = .fastFormat
    for (i, asset) in assets.enumerated() {
        self.imageManager.requestAVAsset(forVideo: asset, options: videoOptions, resultHandler: { (video, audioMix, info) in
            guard let video = video else { return }
            videoArray.remove(at: i)
            videoArray[i] = video
        })
    }
    return videoArray.flatMap { $0 }
}

Giving the array the desired number of items as nil will stop it from erroring when inserting items, and then when the download is complete, remove the existing nil value and replace it with the actual AVAsset.

Finally, flatMap the resulting array to unpack the optionals (and optionally check that you have the desired number of items by comparing it with the incoming assets array).