iosswiftuicollectionviewuicollectionviewcelluicollectionviewdelegate

Reusable data sources in Swift w/ different cell types


Based on this article I have created a reusable data source for UICollectionView as follows :-

final class CollectionViewDataSource<Model>: NSObject, UICollectionViewDataSource {
  typealias CellConfigurator = (Model, UICollectionViewCell) -> Void
  var models: [Model] = []

  private let reuseIdentifier: String
  private let cellConfigurator: CellConfigurator

  init(reuseIdentifier: String, cellConfigurator: @escaping CellConfigurator) {
    self.reuseIdentifier = reuseIdentifier
    self.cellConfigurator = cellConfigurator
  }

  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return models.count
  }

  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let model = models[indexPath.item]
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
    cellConfigurator(model, cell)
    return cell
  }
}

I have then extended this class so I can provide 'cell specific' setup based on the type of model

extension CollectionViewDataSource where Model == HomeFeedItem {
  static func make(reuseIdentifier: String = "FEED_ARTICLE_CELL") -> CollectionViewDataSource {
    return CollectionViewDataSource(reuseIdentifier: reuseIdentifier, cellConfigurator: { item, cell in
      (cell as? FeedArticleCell)?.render(with: item)
    })
  }
}

extension CollectionViewDataSource where Model == HomeFeedAlertItem {
  static func make(reuseIdentifier: String = "FEED_ALERT_CELL") -> CollectionViewDataSource {
    return CollectionViewDataSource(reuseIdentifier: reuseIdentifier, cellConfigurator: { item, cell in
      (cell as? FeedAlertCell)?.render(with: item)
    })
  }
}

This is working perfect, however, each of these cells has a different design but does in fact accept very similar properties (as do the other cells) - because of this I was thinking of creating a simple FeedItemModel and mapping these properties prior to rendering my feed. This would ensure anywhere I rendered a feed item, I was always dealing with the same properties.

With that in mind I tried to create something like :-

extension CollectionViewDataSource where Model == FeedItemModel {
  static func make(reuseIdentifier: String = "FEED_ARTICLE_CELL") -> CollectionViewDataSource {
    return CollectionViewDataSource(reuseIdentifier: reuseIdentifier, cellConfigurator: { item, cell in
      switch item.type {
      case .news: (cell as? FeedArticleCell)?.render(with: item)
      case .alert: (cell as? FeedAlertCell)?.render(with: item)
      }
    })
  }
}

This however falls down as the reuseIdentifier field is no longer correct if item.type is .alert.

How can I refactor this pattern to allow me to use different cells types with the same model? Or should I abandon this approach and stick to a different model for each cell type regardless of the input properties being the same?


Solution

  • You can create a protocol such as

    protocol FeedRenderable {
      var reuseIdentifier: String { get }
    }
    

    Then ensure the Model type conforms to FeedRenderable.

    You can then refactor your CollectionViewDataSource to

    final class CollectionViewDataSource<Model>: NSObject, UICollectionViewDataSource where Model: FeedRenderable {
      typealias CellConfigurator = (Model, UICollectionViewCell) -> Void
      var models: [Model] = []
    
      private let cellConfigurator: CellConfigurator
    
      init(_ cellConfigurator: @escaping CellConfigurator) {
        self.cellConfigurator = cellConfigurator
      }
    
      func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return models.count
      }
    
      func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let model = models[indexPath.item]
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: model.reuseIdentifier, for: indexPath)
        cellConfigurator(model, cell)
        return cell
      }
    }
    

    Notice the following changes

    final class CollectionViewDataSource<Model>: NSObject, UICollectionViewDataSource where Model: FeedRenderable {
    ....
    init(_ cellConfigurator: @escaping CellConfigurator) 
    ....
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: model.reuseIdentifier, for: indexPath)
    ....
    

    You can then ensure whatever model is passed in sets its reuseIdentifier based on the item.type property

    extension GenericFeedItem: FeedRenderable {
      var reuseIdentifier: String {
        switch type {
        case .news: return "FEED_ARTICLE_CELL"
        case .alert: return "FEED_ALERT_CELL"
        }
      }
    }
    

    Your extension then becomes

    extension CollectionViewDataSource where Model == GenericFeedItem {
      static func make() -> CollectionViewDataSource {
        return CollectionViewDataSource() { item, cell in
          (cell as? FeedArticleCell)?.render(with: item)
          (cell as? FeedAlertCell)?.render(with: item)
        }
      }
    }