简体   繁体   中英

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?

You can add a associate identifier to your FeedItemModelType

var reuseIdentifier: String {
    switch self {
        case .news: return "NEW_CELL"
        case .alert: return "ALERT_CELL"
   }
}

And your factory method would look like this

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

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

How about adding a second generic type for the cell

final class CollectionViewDataSource<Model, CellType : UICollectionViewCell>: NSObject, UICollectionViewDataSource {
    typealias CellConfigurator = (Model, CellType) -> 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) as! CellType
        cellConfigurator(model, cell)
        return cell
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM