Post

Visitor Pattern in TableView

Using the Visitor Pattern to enhance the readability and extensibility of TableView

Visitor Pattern in TableView

ℹ️ℹ️ℹ️ The following content is translated by OpenAI.

Click here to view the original Chinese version. | 點此查看本文中文版


Visitor Pattern in TableView

Using the Visitor Pattern to enhance the readability and extensibility of TableView

Photo by [Alex Wong](https://unsplash.com/@killerfvith?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText){:target="_blank"}

Photo by Alex Wong

Introduction

Following the previous article “Visitor Pattern in Swift” which introduced the Visitor Pattern and a simple practical application, this article will discuss another real-world application of this pattern in iOS development.

Requirement Scenario

We need to develop a dynamic wall feature that displays various types of blocks in a dynamic combination.

For example, consider the dynamic wall of StreetVoice:

As shown in the image above, the dynamic wall is composed of various types of blocks:

  • Type A: Event Updates
  • Type B: Follow Recommendations
  • Type C: New Song Updates
  • Type D: New Album Updates
  • Type E: New Follow Updates
  • Type … More

We can expect that the types will continue to increase with future feature iterations.

Problems

Without any architectural design, the code might look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let row = datas[indexPath.row]
    switch row.type {
    case .invitation:
        let cell = tableView.dequeueReusableCell(withIdentifier: "invitation", for: indexPath) as! InvitationCell
        // config cell with viewObject/viewModel...
        return cell
    case .newSong:
        let cell = tableView.dequeueReusableCell(withIdentifier: "newSong", for: indexPath) as! NewSongCell
        // config cell with viewObject/viewModel...
        return cell
    case .newEvent:
        let cell = tableView.dequeueReusableCell(withIdentifier: "newEvent", for: indexPath) as! NewEventCell
        // config cell with viewObject/viewModel...
        return cell
    case .newText:
        let cell = tableView.dequeueReusableCell(withIdentifier: "newText", for: indexPath) as! NewTextCell
        // config cell with viewObject/viewModel...
        return cell
    case .newPhotos:
        let cell = tableView.dequeueReusableCell(withIdentifier: "newPhotos", for: indexPath) as! NewPhotosCell
        // config cell with viewObject/viewModel...
        return cell
    }
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    let row = datas[indexPath.row]
    switch row.type {
    case .invitation:
        if row.isEmpty {
            return 100
        } else {
            return 300
        }
    case .newSong:
        return 100
    case .newEvent:
        return 200
    case .newText:
        return UITableView.automaticDimension
    case .newPhotos:
        return UITableView.automaticDimension
    }
}
  • Difficult to test: It’s hard to determine what logic corresponds to each type.
  • Hard to maintain and extend: When a new type needs to be added, this ViewController must be modified; methods like cellForRow, heightForRow, and willDisplay are scattered across various functions, making it easy to forget to update or mistakenly change something.
  • Hard to read: All logic is embedded within the View.

Visitor Pattern Solution

Why?

After organizing the object relationships, we have the following diagram:

We have many types of DataSources (ViewObjects) that need to interact with various types of operators, which is a classic example of Visitor Double Dispatch.

How?

To simplify the demo code, we will use PlainTextFeedViewObject for plain text updates, MemoriesFeedViewObject for daily memories, and MediaFeedViewObject for image updates to illustrate the design.

The architecture diagram applying the Visitor Pattern is as follows:

First, define the Visitor interface, which abstracts the types of DataSources that the operators can accept:

1
2
3
4
5
6
7
protocol FeedVisitor {
    associatedtype T
    func visit(_ viewObject: PlainTextFeedViewObject) -> T?
    func visit(_ viewObject: MediaFeedViewObject) -> T?
    func visit(_ viewObject: MemoriesFeedViewObject) -> T?
    //...
}

Each operator implements the FeedVisitor interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct FeedCellVisitor: FeedVisitor {
    typealias T = UITableViewCell.Type
    
    func visit(_ viewObject: MediaFeedViewObject) -> T? {
        return MediaFeedTableViewCell.self
    }
    
    func visit(_ viewObject: MemoriesFeedViewObject) -> T? {
        return MemoriesFeedTableViewCell.self
    }
    
    func visit(_ viewObject: PlainTextFeedViewObject) -> T? {
        return PlainTextFeedTableViewCell.self
    }
}

Implementing the mapping between ViewObject and UITableViewCell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct FeedCellHeightVisitor: FeedVisitor {
    typealias T = CGFloat
    
    func visit(_ viewObject: MediaFeedViewObject) -> T? {
        return 30
    }
    
    func visit(_ viewObject: MemoriesFeedViewObject) -> T? {
        return 10
    }
    
    func visit(_ viewObject: PlainTextFeedViewObject) -> T? {
        return 10
    }
}

Implementing the mapping between ViewObject and UITableViewCell Height:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct FeedCellConfiguratorVisitor: FeedVisitor {
    
    private let cell: UITableViewCell
    
    init(cell: UITableViewCell) {
        self.cell = cell
    }
    
    func visit(_ viewObject: MediaFeedViewObject) -> Any? {
        guard let cell = cell as? MediaFeedTableViewCell else { return nil }
        // cell.config(viewObject)
        return nil
    }
    
    func visit(_ viewObject: MemoriesFeedViewObject) -> Any? {
        guard let cell = cell as? MediaFeedTableViewCell else { return nil }
        // cell.config(viewObject)
        return nil
    }
    
    func visit(_ viewObject: PlainTextFeedViewObject) -> Any? {
        guard let cell = cell as? MediaFeedTableViewCell else { return nil }
        // cell.config(viewObject)
        return nil
    }
}

Implementing how to configure the ViewObject to the Cell.

When a new DataSource (ViewObject) needs to be supported, you only need to add a new method to the FeedVisitor interface and implement the corresponding logic in each operator.

Binding DataSource (ViewObject) with operators:

1
2
3
protocol FeedViewObject {
    @discardableResult func accept<V: FeedVisitor>(visitor: V) -> V.T?
}

Implementation of the binding interface in ViewObject:

1
2
3
4
5
6
7
8
9
10
11
struct PlainTextFeedViewObject: FeedViewObject {
    func accept<V>(visitor: V) -> V.T? where V : FeedVisitor {
        return visitor.visit(self)
    }
}

struct MemoriesFeedViewObject: FeedViewObject {
    func accept<V>(visitor: V) -> V.T? where V : FeedVisitor {
        return visitor.visit(self)
    }
}

Implementation in UITableView:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
final class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    
    private let cellVisitor = FeedCellVisitor()
    
    private var viewObjects: [FeedViewObject] = [] {
        didSet {
            viewObjects.forEach { viewObject in
                let cellName = viewObject.accept(visitor: cellVisitor)
                tableView.register(cellName, forCellReuseIdentifier: String(describing: cellName))
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.delegate = self
        tableView.dataSource = self
        
        viewObjects = [
            MemoriesFeedViewObject(),
            MediaFeedViewObject(),
            PlainTextFeedViewObject(),
            MediaFeedViewObject(),
            PlainTextFeedViewObject(),
            MediaFeedViewObject(),
            PlainTextFeedViewObject()
        ]
        // Do any additional setup after loading the view.
    }
}

extension ViewController: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewObjects.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let viewObject = viewObjects[indexPath.row]
        let cellName = viewObject.accept(visitor: cellVisitor)
        
        let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: cellName), for: indexPath)
        let cellConfiguratorVisitor = FeedCellConfiguratorVisitor(cell: cell)
        viewObject.accept(visitor: cellConfiguratorVisitor)
        return cell
    }
}

extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let viewObject = viewObjects[indexPath.row]
        let cellHeightVisitor = FeedCellHeightVisitor()
        let cellHeight = viewObject.accept(visitor: cellHeightVisitor) ?? UITableView.automaticDimension
        return cellHeight
    }
}

Results

  • Testing: Adheres to the Single Responsibility Principle, allowing for testing of each operator’s logic for each data point.
  • Maintenance and Extensibility: When a new DataSource (ViewObject) needs to be supported, simply add a new method to the Visitor protocol and implement it in the respective operator Visitor. If a new operator needs to be extracted, just create a new class to implement it.
  • Readability: By reviewing each operator object, you can easily understand the composition logic of each View on the entire page.

Complete Project

Note…

This article was written during a period of low inspiration in July 2022. If there are any inaccuracies or errors in the content, please feel free to point them out!

Further Reading

If you have any questions or feedback, feel free to contact me.


This article was first published on Medium ➡️ Click Here

Automatically converted and synchronized using ZMediumToMarkdown and Medium-to-jekyll-starter.

Improve this page on Github.

Buy me a beer

239 Total Views
Last Statistics Date: 2025-03-11 | 153 Views on Medium.
This post is licensed under CC BY 4.0 by the author.