Some context: I have a UICollectionView
which will display around a thousand tiny images, though only around 100 of them will be visible at the same time.
I need to load this images from the disk in a separate thread, so that the UI is not blocked and the user can interact with the app while the images are still appearing.
To do so, I've implemented rob mayoff's answer to Proper way to deal with cell reuse with background threads? in Swift as follows, where PotoCell
is a subclass of UICollectionViewCell
:
var myQueue = dispatch_queue_create("com.dignityValley.Autoescuela3.photoQueue", DISPATCH_QUEUE_SERIAL)
var indexPathsNeedingImages = NSMutableSet()
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! PhotoCell
cell.imageView.image = nil
indexPathsNeedingImages.addObject(indexPath)
dispatch_async(myQueue) { self.bg_loadOneImage() }
return cell
}
func bg_loadOneImage() {
var indexPath: NSIndexPath?
dispatch_sync(dispatch_get_main_queue()) {
indexPath = self.indexPathsNeedingImages.anyObject() as? NSIndexPath
if let indexPath = indexPath {
self.indexPathsNeedingImages.removeObject(indexPath)
}
}
if let indexPath = indexPath {
bg_loadImageForRowAtIndexPath(indexPath)
}
}
func bg_loadImageForRowAtIndexPath(indexPath: NSIndexPath) {
if let cell = self.cellForItemAtIndexPath(indexPath) as? PhotoCell {
if let image = self.photoForIndexPath(indexPath) {
dispatch_async(dispatch_get_main_queue()) {
cell.imageView.image = image
self.indexPathsNeedingImages.removeObject(indexPath)
}
}
}
}
func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
indexPathsNeedingImages.removeObject(indexPath)
}
However, I'm not getting acceptable results: when I scroll, the UI freezes for a fraction of a second while images are being loaded in the background. Scrolling is not smooth enough. Moreover, while the first 100 images are being loaded, I am not able to scroll at all until the last image has been displayed. The UI is still being blocked after the implementation of multithreading.
Surprisingly, I can achieve the desired smoothness by modifying my queue to:
var myQueue = dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)
Note that previosly I was using a custom serial queue.
After this change, the UI is fully responsive, but now I have a very serious problem: my app crashes occaionally, and I think it has to do with the fact that several threads may be accessing and modifying indexPathsNeedingImages
at the same time.
Trying to use locks/syncronization makes my images end into the wrong cells some times. So, I would like to achieve the smoothness that gives me a global background queue but using a cutom serial queue. I can't figure out why my UI is freezing when I use a custom serial queue and why it is not when I use a global one.
Some thoughts: maybe setting cell.imageView.image = image
for around 100 cells takes some time even though image
has been already allocated. The problem may be here, since commenting this line makes the scroll way smoother. But what I don't understand is why scrolling is smooth when I leave this line uncommented and I use a global background-priority queue (until the app crashes throwing a message telling that ... was mutated while being enumerated
).
Any ideas on how to tackle this?