XE2V Project Harvest series:

Tensions: To make the UITableView and UICollectionView more usable

YLStateMachine: a simple state machine

YLRefreshKit: With it, you can delete your refresh code

Decomposer: a protocol-oriented architecture pattern

Protocol oriented programming and operation automation ── taking refresh automation as an example

preface

XE2V is a V2EX client, and as my first project, I really wanted to get it right. It seems like such a common wish, but once you get started, it turns out that writing code that makes you happy isn’t as easy as it seems, and the project is still in a state of unfinished business.

Because of my lack of experience and my own stupidity, a lot of things that are easy for the average developer became a big problem for me. There’s so much I don’t know, and my way of dealing with it is, well, avoiding it when I can. Procrastination became the norm. But projects always have to be finished, and I have to continue at some point. Pick up the project is always left look right look not pleasing to the eye, really abomination, heart a horizontal, put the project over again. Time, then, becomes an endless cycle of procrastination and rewriting. Fortunately, in the rewrite of the project, some problems were eventually solved, and I extracted them into a library to share with you. Due to the limitations of our level, there are certainly many deficiencies. We welcome your comments and corrections.

Question raising

When a UITableView or UICollectionView page contains multiple types of cells, registering and configuring these cells requires a lot of repetitive code. For example, a table View page contains four types of cells: ACell, BCell, CCell, and DCell. In the tableView(_:cellForRowAt:) method, we might write:

func tableView(_ tableView: UITableView.cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    switch indexPath.section {
    case 0:
        let cell = tableView.dequeueReusableCell(withIdentifier: "ACell", for: indexPath) as! ACell
        cell.configure(model[indexPath.section][indexPath.row])
        return cell
    case 1:
        let cell = tableView.dequeueReusableCell(withIdentifier: "BCell", for: indexPath) as! BCell
        cell.configure(model[indexPath.section][indexPath.row])
        return cell
    case 2:
        let cell = tableView.dequeueReusableCell(withIdentifier: "CCell", for: indexPath) as! CCell
        cell.configure(model[indexPath.section][indexPath.row])
        return cell
    case 3:
        let cell = tableView.dequeueReusableCell(withIdentifier: "DCell", for: indexPath) as! DCell
        cell.configure(model[indexPath.section][indexPath.row])
        return cell
    }
}
Copy the code

Repeating the same pattern four times is just not elegant. Ideally, the tableView(_:cellForRowAt:) method would look something like this:

func tableView(_ tableView: UITableView.cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(.)
    cell.configure(data[indexPath.section][indexPath.row])
    return cell
}
Copy the code

So, can we find a way to achieve the above effect?

To simplify thetableView(_:cellForRowAt:)methods

First, we want to make the dequeueReusableCell(…) Methods can return different cells under different identifiers. How? Opaque types are designed to solve this kind of problem. To do this, we add an extension to UITableView:

extension UITableView {
    func dequeueReusableCell(
        for indexPath: IndexPath.with identifiers: [String]) -> some UITableViewCell {
        for (index, identifier) in identifiers.enumerated() where index = = indexPath.section {
            let cell = dequeueReusableCell(withIdentifier: identifier, for: indexPath)
            return cell
        }
        
        fatalError()}}Copy the code

Next, each type of cell should have a configure(_:) method, which is easy to do by extending the UITableViewCell:

@objc protocol Configurable {
    func configure(_ model: Any?)
}

extension UITableViewCell: Configurable {
    func configure(_ model: Any?){}}Copy the code

So in the tableView(_:cellForRowAt:) method we can write:

func tableView(_ tableView: UITableView.cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(for: indexPath, with: ["ACell"."BCell"."CCell"."DCell"])
    cell.configure(model[indexPath.section][indexPath.row])
    return cell
}
Copy the code

saidIdentifierA better way

Strings are prone to misspellings. Is there a better way to represent identifiers? One solution is to add an identifier attribute to the cell so that we can take advantage of Xcode’s auto-completion to help us avoid errors. We can do this:

protocol ReusableView {}extension ReusableView {
    static var reuseIdentifier: String {
        return String(describing: self)}}extension UITableViewCell: ReusableView {}Copy the code

Then, use the tableView(_:cellForRowAt:) method:

func tableView(_ tableView: UITableView.cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(for: indexPath, with: [ACell.reuseIdentifier, BCell.reuseIdentifier, CCell.reuseIdentifier, DCell.reuseIdentifier])
    cell.configure(model[indexPath.section][indexPath.row])
    return cell
}
Copy the code

Simplify cell registration

Another area where duplicate code occurs is in cell registration. For example, when A, B, C, or D type cells are created in pure code, we register them like this:

tableView.register(ACell.self, forCellReuseIdentifier: ACell.reuseIdentifier)
tableView.register(BCell.self, forCellReuseIdentifier: BCell.reuseIdentifier)
tableView.register(CCell.self, forCellReuseIdentifier: CCell.reuseIdentifier)
tableView.register(DCell.self, forCellReuseIdentifier: DCell.reuseIdentifier)
Copy the code

Can the registration process be simplified?

If you look closely at the registration method, all you need to do is give it an argument of Type uitableViewCell.type. Based on this, we can add an extension to UITableView that looks like this:

extension UITableView {
    func registerCells(with cells: [UITableViewCell.Type]) {
        for cell in cells {
            register(cell, forCellReuseIdentifier: cell.reuseIdentifier)
        }
    }
}
Copy the code

Thus, a single line of code is required to register:

tableView.registerCells(with: [ACell.self.BCell.self.CCell.self.DCell.self])
Copy the code

What if cells are created using NIB? That’s easy. Let’s first extend the UITableViewCell:

protocol NibView {}extension NibView where Self: UIView {
    static var nib: UINib {
        return UINib(nibName: String(describing: self), bundle: nil)}}extension UITableViewCell: NibView {}Copy the code

Add another extension to UITableView:

extension UITableView {
    func registerNibs(with cells: [UITableViewCell.Type]) {
        for cell in cells {
            register(cell.nib, forCellReuseIdentifier: cell.reuseIdentifier)
        }
    }
}
Copy the code

We can then register niB-created cells in a similar manner.

In this way, the problem is solved. However, if you look at the dequeueReusableCell(for: With 🙂 method and registerCells(with:) method, their arguments feel, well, not pretty. Is there a better way to write it? Well, we can put them in the table View’s Model properties and just call them when we use them.

Define Model

Oh, the Model! After all this talk, we haven’t thought about it.

What is a Model? You might say it’s something that provides data. Indeed, we generally use the Model as a data provider. The Model will have a read-only property of data that provides the data, and the type of data should be [[Any]], taking into account cell grouping and data type differences.

Is that all the Model does? In fact, for UITableView and UICollectionView’s Model, it can hold a lot more. Each table view has a Model and several types of cells. Therefore, the relationship between the Model and the cell can be established. We can store the type of the cell in the Model and use it when needed. Note that registering cells usually precedes Model instantiation, so cell types should be stored in Model class methods. In addition, the Table View can be paginated, so it’s a good idea for the Model to have a nextPage property.

With the above discussion, let’s define model:

protocol Pageable {
    var nextPage: Int? { get}}extension Pageable {
    var nextPage: Int? { nil}}protocol ModelType: Pageable {
    static var tCells: [UITableViewCell.Type]? { get }
    static var tNibs: [UITableViewCell.Type]? { get }
    // All cell types, sort by display order
    static var tAll: [UITableViewCell.Type]? { get }
    
    // Store model data in display order
    var data: [[Any]] { get}}extension ModelType {
    static var tCells: [UITableViewCell.Type]? { nil }
    static var tNibs: [UITableViewCell.Type]? { nil }
    static var tAll: [UITableViewCell.Type]? { nil}}Copy the code

use

So, we can do this with UITableView in the future:

First, make the Model follow the ModelType:

struct SomeModel: ModelType {
    let someA: [A]
    let someB: [B]
    let someC: [C]
    let someD: [D]
    
    var data: [[Any]] {
    	return [someA, someB, someC, someD]
    }
}

extension SomeModel {
    static var tCells: [UITableViewCell.Type]?{[ACell.self.BCell.self]}static var tNibs: [UITableViewCell.Type]?{[CCell.self.DCell.self]}static var tAll: [UITableViewCell.Type]? {
        // Sort by display order
        [ACell.self.BCell.self.CCell.self.DCell.self]}}Copy the code

Next, implement the configure(_:) method in the cell:

class SomeCell: UITableViewCell {
    .
    // Configure cell
    override func configure(_ model: Any?) {
        .}}Copy the code

Finally, in ViewController:

1. Create the Model objectlet someModel = SomeModel(.)

2Registered cell.override func viewDidLoad(a) {
    super.viewDidLoad()
    .
    tableView.registerCells(with: SomeModel.tCells!)
    tableView.registerNibs(with: SomeModel.tNibs!)}3. Create and configure the cellfunc tableView(_ tableView: UITableView.cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(for: indexPath, with: SomeModel.tAll!)
    cell.configure(someModel.data[indexPath.section][indexPath.row])
    return cell
}
Copy the code

Here are some changes to the dequeueReusableCell(for:with:) method:

func dequeueReusableCell(
    for indexPath: IndexPath.with cells: [UITableViewCell.Type]) -> some UITableViewCell {
    for (index, cell) in cells.enumerated() where index = = indexPath.section {
        let cell = dequeueReusableCell(withIdentifier: cell.reuseIdentifier, for: indexPath)
        return cell
    }

    fatalError()}Copy the code

UICollectionView has a similar solution that I won’t cover.

The next trailer

In order to realize the automatic refresh function, I need to use the state machine. After a cursory review of several Swift versions of the state machine on GitHub, I was not satisfied with them (in fact, I did not understand 😅️), so I wrote one myself. In the next article, I’ll show you how to create a state machine from scratch.

YLExtensions