RxSwift Race Condition on Cells

2016-07-04
#swift #rxswift #macOS

A few days ago I wrote a simple code to asynchronously load an image using RxMoya. RxMoya is an RxSwift wrapper for Moya. The flow is simple, everytime collection view asks for NSCollectionViewItem (yes, I'm building a macOS app), I need to load the image asynchronously. Here's a part of the implementation:

func collectionView(
  collectionView: NSCollectionView,
  itemForRepresentedObjectAtIndexPath indexPath: NSIndexPath
) -> NSCollectionViewItem {
  let item = collectionView.makeItemWithIdentifier(
    "ChapterItem",
    forIndexPath: indexPath
  ) as! ChapterItem

  let chapter = vm[indexPath.item]
  let chapterPageVm = ChapterPagesViewModel(..)

  // Fetch manga chapter pages
  chapterPageVm.fetch()

  chapterPageVm
    .chapterPages
    .drive(onNext: { _ in
      // After chapter pages are loaded
      // we will set chapter preview here
    })
    .addDisposableTo(disposeBag)

  ...

  return item
}

Problem analysis

Obviously there's a problem with the implementation, how do I make sure that after the chapter pages are loaded, the execution context of driveNext is valid? In other words, how to make sure that the cell item has not been re-used (because if it is and Iā€™m setting the fetched data, then there will be race condition)?

A solution that might work is by setting a DisposeBag to each cell and disposing it everytime didEndDisplayingItem gets called.

The solution

Sounds like a perfect plan! Now I just need to find a way to set the dispose bags and keep track of them for each cell. Luckily RxSwift has a great community around it, dpaschich on rxswift.slack.com suggested to set DisposeBag on each of the cell item and disposing it using a custom method didEndDisplaying

class ChapterItem: NSCollectionViewItem {
  @IBOutlet weak var chapterTitle: NSTextField!
  @IBOutlet weak var chapterNumber: NSTextField!
  @IBOutlet weak var chapterPreview: NSImageView!

  var disposeBag = DisposeBag()

  func didEndDisplaying() {
    chapterPreview.image = .None

    disposeBag = DisposeBag()
  }
}
func collectionView(
  collectionView: NSCollectionView,
  didEndDisplayingItem item: NSCollectionViewItem,
  forRepresentedObjectAtIndexPath indexPath: NSIndexPath
) {
  let _item = item as! ChapterItem

  _item.didEndDisplaying()
}

func collectionView(
  collectionView: NSCollectionView,
  itemForRepresentedObjectAtIndexPath indexPath: NSIndexPath
) -> NSCollectionViewItem {
  let item = collectionView.makeItemWithIdentifier(
    "ChapterItem",
    forIndexPath: indexPath
  ) as! ChapterItem

  let chapter = vm[indexPath.item]
  let chapterPageVm = ChapterPagesViewModel(..)

  // Fetch manga chapter pages
  chapterPageVm.fetch()

  // Notice how I add disposable to item.disposeBag
  chapterPageVm
    .chapterPages
    .driveNext { _ in
      // After chapter pages are loaded
      // we will set chapter preview here
    } >>> item.disposeBag

  ...

  return item
}

Topics that might interest you