3

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?

Community
  • 1
  • 1
Aleix Pinardell
  • 271
  • 2
  • 8

2 Answers2

0

in this function it should be dispatch_ASYNC to the main queue. The only reason you should switch queues is when getting data off a network or doing a task that takes time and blocks the ui. When asyncing back to the main queue, ui related things should be done.

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)
}
}

Also if let indexPath = indexPath is confusing

standa
  • 26
  • 4
0

After quite some time, I've managed to make it work. Although now I'm testing on an iPad Air 1, and before it was an iPad 2, don't know if this could have some influence on "responsiveness".

Anyway, here's the code. In your UIViewController:

class TestViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

    var collectionView = UICollectionView(frame: CGRectZero, collectionViewLayout: UICollectionViewFlowLayout())
    var myQueue = dispatch_queue_create("com.DignityValley.Autoescuela-3.photoQueue", DISPATCH_QUEUE_SERIAL)
    var indexPathsNeedingImages = NSMutableSet()

    ...

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier(ReuseIdentifiers.testCell, forIndexPath: indexPath) as! TestCell
        ...
        cell.imageIdentifiers = record.imageIdentifiers
        indexPathsNeedingImages.addObject(indexPath)
        dispatch_async(myQueue) { self.bgLoadOneCell() }
        return cell
    }

    func bgLoadOneCell() {
        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 {
            loadImagesForCellAtIndexPath(indexPath)
        }
    }

    func loadImagesForCellAtIndexPath(indexPath: NSIndexPath) {
        if let cell = collectionView.cellForItemAtIndexPath(indexPath) as? TestCell {
            cell.displayImages()
        }
    }

    ...
}

And in your UICollectionViewCell subclass:

class TestCell: UICollectionViewCell {

    ...

    var photosView = UIView()
    var imageIdentifiers: [String?] = [] {

    func displayImages() {
        for i in 0..<imageIdentifiers.count {
            var image: UIImage?
            if let identifier = imageIdentifiers[i] {
                image = UIImage(named: "test\(identifier).jpg")
            }
            dispatch_sync(dispatch_get_main_queue()) {
                self.updateImageAtIndex(i, withImage: image)
            }
        }
    }

    func updateImageAtIndex(index: Int, withImage image: UIImage?) {
        for imageView in photosView.subviews {
            if let imageView = imageView as? UIImageView {
                if imageView.tag == index {
                    imageView.image = image
                    break
                }
            }
        }
    }

    ...
}

I think the only difference between this and what I had before is the call to dispatch_sync rather than dispatch_async when actually updating the image (i.e. imageView.image = image).

Aleix Pinardell
  • 271
  • 2
  • 8