Created
April 9, 2021 18:39
-
-
Save atierian/425b9355245617796905973f1b59f276 to your computer and use it in GitHub Desktop.
Multiple UITableViewCell subclasses no logical branches in View objects
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Inspiration from TweetleDumb by Ian Keen | |
| final class FooCell: UITableViewCell { | |
| let label = UILabel() | |
| override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { | |
| super.init(style: style, reuseIdentifier: reuseIdentifier) | |
| layout() | |
| } | |
| required init?(coder: NSCoder) { | |
| super.init(coder: coder) | |
| layout() | |
| } | |
| private func layout() { | |
| label.translatesAutoresizingMaskIntoConstraints = false | |
| contentView.addSubview(label) | |
| let guide = contentView.safeAreaLayoutGuide | |
| let constraints = [ | |
| label.topAnchor.constraint(equalTo: guide.topAnchor), | |
| label.leadingAnchor.constraint(equalTo: guide.leadingAnchor), | |
| label.bottomAnchor.constraint(equalTo: guide.bottomAnchor), | |
| label.trailingAnchor.constraint(equalTo: guide.trailingAnchor) | |
| ] | |
| NSLayoutConstraint.activate(constraints) | |
| } | |
| } | |
| extension FooCell: ViewModelConfigurable { | |
| func configure(with viewModel: FooCellViewModel) { | |
| label.text = viewModel.labelText | |
| } | |
| } | |
| struct FooCellViewModel { | |
| let labelText: String | |
| } | |
| extension FooCellViewModel: TableViewCellRepresentable { | |
| typealias TableViewCell = FooCell | |
| } | |
| class BarCell: UITableViewCell { | |
| let customImageView = UIImageView() | |
| override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { | |
| super.init(style: style, reuseIdentifier: reuseIdentifier) | |
| layout() | |
| } | |
| required init?(coder: NSCoder) { | |
| super.init(coder: coder) | |
| layout() | |
| } | |
| private func layout() { | |
| customImageView.translatesAutoresizingMaskIntoConstraints = false | |
| contentView.addSubview(customImageView) | |
| let guide = contentView.safeAreaLayoutGuide | |
| let constraints = [ | |
| customImageView.topAnchor.constraint(equalTo: guide.topAnchor), | |
| customImageView.leadingAnchor.constraint(equalTo: guide.leadingAnchor), | |
| customImageView.bottomAnchor.constraint(equalTo: guide.bottomAnchor), | |
| customImageView.trailingAnchor.constraint(equalTo: guide.trailingAnchor) | |
| ] | |
| NSLayoutConstraint.activate(constraints) | |
| } | |
| } | |
| extension BarCell: ViewModelConfigurable { | |
| func configure(with viewModel: BarCellViewModel) { | |
| customImageView.image = UIImage(systemName: viewModel.systemImageName) | |
| } | |
| } | |
| struct BarCellViewModel { | |
| let systemImageName: String | |
| } | |
| extension BarCellViewModel: TableViewCellRepresentable { | |
| typealias TableViewCell = BarCell | |
| } | |
| class BazCell: UITableViewCell { | |
| let textField = UITextField() | |
| override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { | |
| super.init(style: style, reuseIdentifier: reuseIdentifier) | |
| layout() | |
| } | |
| required init?(coder: NSCoder) { | |
| super.init(coder: coder) | |
| layout() | |
| } | |
| private func layout() { | |
| textField.translatesAutoresizingMaskIntoConstraints = false | |
| contentView.addSubview(textField) | |
| let guide = contentView.safeAreaLayoutGuide | |
| let constraints = [ | |
| textField.topAnchor.constraint(equalTo: guide.topAnchor), | |
| textField.leadingAnchor.constraint(equalTo: guide.leadingAnchor), | |
| textField.bottomAnchor.constraint(equalTo: guide.bottomAnchor), | |
| textField.trailingAnchor.constraint(equalTo: guide.trailingAnchor) | |
| ] | |
| NSLayoutConstraint.activate(constraints) | |
| } | |
| } | |
| extension BazCell: ViewModelConfigurable { | |
| func configure(with viewModel: BazCellViewModel) { | |
| textField.placeholder = viewModel.placeholder | |
| textField.text = viewModel.text | |
| } | |
| } | |
| struct BazCellViewModel { | |
| let placeholder: String | |
| var text: String? | |
| } | |
| extension BazCellViewModel: TableViewCellRepresentable { | |
| typealias TableViewCell = BazCell | |
| } | |
| class MyViewModel { | |
| let cells: [TableViewCellViewModel.Type] = [ | |
| FooCellViewModel.self, | |
| BarCellViewModel.self, | |
| BazCellViewModel.self | |
| ] | |
| let dataSet: [TableViewCellViewModel] = [ | |
| FooCellViewModel(labelText: "Foo"), | |
| BarCellViewModel(systemImageName: "barcode"), | |
| BazCellViewModel(placeholder: "example", text: nil) | |
| ] | |
| } | |
| class MyViewController: UIViewController, UITableViewDataSource { | |
| let tableView = UITableView(frame: .zero, style: .insetGrouped) | |
| let viewModel = MyViewModel() | |
| override func viewDidLoad() { | |
| super.viewDidLoad() | |
| tableView.translatesAutoresizingMaskIntoConstraints = false | |
| view.addSubview(tableView) | |
| tableView.register(cells: viewModel.cells) | |
| tableView.dataSource = self | |
| let guide = view.safeAreaLayoutGuide | |
| let constraints = [ | |
| tableView.topAnchor.constraint(equalTo: guide.topAnchor), | |
| tableView.leadingAnchor.constraint(equalTo: guide.leadingAnchor), | |
| tableView.bottomAnchor.constraint(equalTo: guide.bottomAnchor), | |
| tableView.trailingAnchor.constraint(equalTo: guide.trailingAnchor) | |
| ] | |
| NSLayoutConstraint.activate(constraints) | |
| } | |
| func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
| viewModel.dataSet.count | |
| } | |
| func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
| viewModel.dataSet[indexPath.row].dequeue(from: tableView, at: indexPath) | |
| } | |
| } | |
| // Reusable.swift | |
| protocol Reusable { | |
| static var reuseIdentifier: String { get } | |
| } | |
| extension Reusable { | |
| static var reuseIdentifier: String { String(describing: self) } | |
| } | |
| extension UITableViewCell: Reusable { } | |
| // TableViewCellViewModel.swift | |
| protocol TableViewCellViewModel { | |
| static func register(with tableView: UITableView) | |
| func dequeue(from tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell | |
| func selected() | |
| } | |
| extension TableViewCellViewModel { | |
| func selected() { } | |
| } | |
| // TableViewCellRepresentable.swift | |
| protocol TableViewCellRepresentable: TableViewCellViewModel { | |
| associatedtype TableViewCell: UITableViewCell | |
| } | |
| extension TableViewCellRepresentable where TableViewCell: Reusable { | |
| static func register(with tableView: UITableView) { | |
| tableView.register(TableViewCell.self, forCellReuseIdentifier: TableViewCell.reuseIdentifier) | |
| } | |
| func dequeue(from tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { | |
| return tableView.dequeueReusableCell(withIdentifier: TableViewCell.reuseIdentifier, for: indexPath) | |
| } | |
| } | |
| extension UITableView { | |
| func register(cells: [TableViewCellViewModel.Type]) { | |
| cells.forEach { $0.register(with: self) } | |
| } | |
| } | |
| // ViewModelConfigurable.swift | |
| protocol ViewModelConfigurable { | |
| associatedtype ViewModel | |
| func configure(with viewModel: ViewModel) | |
| } | |
| extension TableViewCellRepresentable where TableViewCell: ViewModelConfigurable & Reusable, TableViewCell.ViewModel == Self { | |
| func dequeue(from tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { | |
| guard | |
| let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.reuseIdentifier, for: indexPath) as? TableViewCell | |
| else { fatalError("Unable to dequeue a cell of type '\(TableViewCell.self)'") } | |
| cell.configure(with: self) | |
| return cell | |
| } | |
| } | |
| let myViewController = MyViewController() | |
| PlaygroundPage.current.liveView = myViewController |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment