Skip to content

Instantly share code, notes, and snippets.

@atierian
Created April 9, 2021 18:39
Show Gist options
  • Select an option

  • Save atierian/425b9355245617796905973f1b59f276 to your computer and use it in GitHub Desktop.

Select an option

Save atierian/425b9355245617796905973f1b59f276 to your computer and use it in GitHub Desktop.
Multiple UITableViewCell subclasses no logical branches in View objects
// 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