Visitor Pattern in TableView
Using Visitor Pattern to Improve TableView Readability and Extensibility

Photo by Alex wong
Introduction
Following the previous article “Visitor Pattern in Swift” which introduced the Visitor pattern and a simple practical use case, this article will present another real-world application in iOS development.
Requirement Scenario
To develop a dynamic wall feature, multiple types of blocks need to be dynamically combined and displayed.
Taking StreetVoice’s dynamic wall as an example:

As shown in the above image, the dynamic wall is composed of various types of blocks dynamically combined:
-
Type A: Event Feed
-
Type B: Tracking Recommendations
-
Type C: New Song Feed
-
Type D: New Album Updates
-
Type E: New Follow-up Activity
-
Type …. More
The types are expected to increase as features iterate in the future.
Problem
Without any architectural design, the code might look like this:
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
}
}
-
Hard to test: It is difficult to verify which Type corresponds to which logic output.
-
Difficult to extend and maintain: Whenever a new Type needs to be added, this ViewController must be modified; cellForRow, heightForRow, willDisplay… are spread across multiple functions, making it easy to forget or make mistakes during updates.
-
Hard to read: All logic resides within the View itself
Visitor Pattern Solution
Why?
The object relationships are organized as shown in the diagram below:

We have many types of DataSource (ViewObject) that need to interact with various types of operators, which is a typical example of Visitor Double Dispatch.
How?
To simplify the demo code, the design uses PlainTextFeedViewObject for plain text feeds, MemoriesFeedViewObject for daily memories, and MediaFeedViewObject for image feeds.
The architecture diagram applying the Visitor Pattern is as follows:

First, define the Visitor interface. This interface abstracts the types of DataSource that the visitor can accept:
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:
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 ViewObject <-> UITableViewCell Mapping.
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 ViewObject <-> UITableViewCell Height Mapping.
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 ViewObject <-> Cell Configuration Mapping.
When a new DataSource (ViewObject) needs to be supported, simply add a new method to the FeedVisitor interface and implement the corresponding logic in each operator.
Binding DataSource (ViewObject) and Operators:
protocol FeedViewObject {
@discardableResult func accept<V: FeedVisitor>(visitor: V) -> V.T?
}
ViewObject Implementation Binding Interface:
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)
}
}
struct MediaFeedViewObject: FeedViewObject {
func accept<V>(visitor: V) -> V.T? where V : FeedVisitor {
return visitor.visit(self)
}
}
Implementation in UITableView:
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
}
}
Result
-
Testing: Complies with the Single Responsibility Principle, allowing tests for each data item of every operator individually.
-
Extensibility and Maintenance: When supporting a new DataSource (ViewObject), simply add a new method to the Visitor protocol and implement it in the respective Visitor operators. If a new operator needs to be separated, just create a new class to implement it.
-
Reading: You only need to browse each operator object to understand the composition logic of each View on the entire page.
Complete Project
Murmur…
An article written during a mental low point in July 2022. Please kindly forgive any incomplete descriptions or errors!



Comments