0

I am currently building a gallery of user photos into an app. So far I simply listed all the user's photos in a UICollectionView. Now I would like to add moment clusters as sections, similar to the iOS Photos app.

What I am doing (a bit simplified):

let momentClusters = PHCollectionList.fetchMomentLists(with: .momentListCluster, options: options)
momentClusters.enumerateObjects { (momentCluster, _, _) in
    let moments = PHAssetCollection.fetchMoments(inMomentList: momentCluster, options: nil)
    var assetFetchResults: [PHFetchResult<PHAsset>] = []
    moments.enumerateObjects { (moment, _, _) in
        let fetchResult = PHAsset.fetchAssets(in: moment, options: options)
        assetFetchResults.append(fetchResult)
    }
    //save assetFetchResults somewhere and use it in UICollectionView methods
}

Turns out this is A LOT more time-intensive than what I did before - up to a minute compared to about 2 seconds on my iPhone X (with a gallery of about 15k pictures). Obviously, this is unacceptable.

Why is the performance of fetching moments so bad, and how can I improve it? Am I using the API wrong?

I tried loading assets on-demand, but it's very difficult, since I then have to work with estimated item counts per moment, and reload sections while the user is scrolling - I couldn't get this to work in a way that is satisfactory (smooth scrolling, no noticable reload).

Any help? How is this API supposed to work? Am I using it wrong.

Update / Part solution

So after playing around, it turns out that the following was a big part of the problem: I was fetching assets using options with a sort descriptor:

let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue)
let assets = PHAsset.fetchAssets(in: moment, options: options)

It seems sorting doesn't allow PhotoKit to make use of indices or caches it has internally. Removing the sortDescriptors speeds up the fetch significantly. It's still slower than before and any further tips are appreciated, but this makes loading times way more bearable.

Note that, without the sort descriptor, assets will be returned oldest ones first, but this can be easily fixed manually by retrieving assets in reversed order in cellForItemAt: (so the cell at 0,0 will get the last asset of the first moment)

BlackWolf
  • 5,239
  • 5
  • 33
  • 60

1 Answers1

1

Disclaimer: Performance-related answers are necessarily speculative...

You've described two extremes:

  • Prefetch all assets for all moments before displaying the collection
  • Fetch all assets lazily, use estimated counts and reload

But there are in-between options. For example, you can fetch only the moments at first, then let fetching assets per moment be driven by the collection view. But instead of waiting until moments are visible before fetching their contents, use the UICollectionViewDataSourcePrefetching protocol to decide what to start fetching before those items are due to be visible on screen.

rickster
  • 124,678
  • 26
  • 272
  • 326
  • Thank you. I didn't know about that protocol actually and I will try to improve the collection view's behaviour with it :-) In this particular case, though, a large part of the pain is that a lot of work is required just to find out how much items are in each section (= moment), and I don't think `DataSourcePrefetching` can help much there :/ – BlackWolf Jul 13 '18 at 21:56
  • You'll still need to estimate section counts and update sections with data source prefetching, but: 1. You can at least do your items-in-section-count updates while the section is offscreen (when you get a `prefetchItemsAt` call referencing a section you haven't loaded, that's your signal to fetch it). 2. You can probably get smoother updates with the UICollection add/remove/update items methods instead of reloading sections. – rickster Jul 13 '18 at 22:33
  • Unfortunately, I cannot get this to work reliably in practice. It is mentioned in Apple's documentation that `prefetchItemsAt` might not be called for every index path that comes into view. And in fact, if I scroll fast through the collection view, I will at some point encounter a section that comes onscreen that has not been prefeteched, which can lead to a crash due to the estimated count being larger than the actual count. – BlackWolf Jul 14 '18 at 22:30
  • updated the question with a part-solution I figured out – BlackWolf Jul 15 '18 at 12:36