Visitor Pattern in iOS (Swift)
An analysis of the practical application of the Visitor Design Pattern in iOS development
ℹ️ℹ️ℹ️ The following content is translated by OpenAI.
Click here to view the original Chinese version. | 點此查看本文中文版
Visitor Pattern in Swift (Sharing Objects Example)
An analysis of the practical application of the Visitor Pattern (in sharing products, songs, articles, etc. to platforms like Facebook, Line, LinkedIn…)
Photo by Daniel McCullough
Introduction
It’s been over 10 years since I first learned about “Design Patterns,” and I still can’t confidently say that I fully grasp them. I’ve often skimmed through all the patterns multiple times, but without internalizing or applying them in practice, I quickly forget what I’ve learned.
I really feel useless.
Skills and Techniques
I once came across a great analogy: the techniques (like PHP, Laravel, iOS, Swift, SwiftUI, etc.) are relatively easy to switch between and learn. However, the foundational skills (like algorithms, data structures, design patterns, etc.) are much harder to master. These foundational skills and techniques complement each other; while techniques are easy to learn, foundational skills are difficult to develop. Just because someone is skilled in techniques doesn’t mean they have strong foundational skills, but those with strong foundational skills can quickly learn techniques. Therefore, rather than saying they complement each other, it’s more accurate to say that foundational skills are the basis, and techniques enhance their effectiveness.
Finding a Suitable Learning Method
Based on my previous learning experiences, I believe that the best way for me to learn Design Patterns is to focus on mastering a few patterns first. I need to internalize and apply them flexibly, developing an intuition for which scenarios are suitable for which patterns. Then, I can gradually accumulate new patterns until I master them all. I think the best approach is to seek out practical scenarios and learn from application.
Learning Resources
I recommend two free learning resources:
- https://refactoringguru.cn/: A comprehensive introduction to all pattern structures, scenarios, and relationships.
- https://shirazian.wordpress.com/2016/04/11/design-patterns-in-swift/: The author introduces various patterns through actual iOS development scenarios, and this article will also follow that direction.
Visitor — Behavioral Patterns
The first chapter documents the Visitor Pattern, which I discovered during my year at StreetVoice. The StreetVoice App has many instances where the Visitor Pattern effectively solves architectural problems. Through this experience, I grasped the essence of the Visitor Pattern, so I decided to write about it in this chapter!
What is a Visitor?
First, let’s understand what a Visitor is. What problem does it aim to solve? What is its structural composition?
Image from refactoringguru
I won’t repeat the details here; please refer directly to refactoringguru’s explanation of the Visitor Pattern.
iOS Practical Scenario — Sharing Functionality
Let’s assume we have the following models: UserModel, SongModel, and PlaylistModel. We want to implement a sharing feature that allows sharing to Facebook, Line, and Instagram. Each model needs to present different sharing messages, and each platform requires different data:
The combination scenario is shown in the image above. The first table displays the customized content for each model, while the second table shows the data required by each sharing platform.
Especially for Instagram, sharing a Playlist requires multiple images, which is different from the data needed for other sharing types.
Defining the Models
First, let’s define the properties for each model:
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
// Model
struct UserModel {
let id: String
let name: String
let profileImageURLString: String
}
struct SongModel {
let id: String
let name: String
let user: UserModel
let coverImageURLString: String
}
struct PlaylistModel {
let id: String
let name: String
let user: UserModel
let songs: [SongModel]
let coverImageURLString: String
}
// Data
let user = UserModel(id: "1", name: "Avicii", profileImageURLString: "https://zhgchg.li/profile/1.png")
let song = SongModel(id: "1",
name: "Wake Me Up",
user: user,
coverImageURLString: "https://zhgchg.li/cover/1.png")
let playlist = PlaylistModel(id: "1",
name: "Avicii Tribute Concert",
user: user,
songs: [
song,
SongModel(id: "2", name: "Waiting for Love", user: UserModel(id: "1", name: "Avicii", profileImageURLString: "https://zhgchg.li/profile/1.png"), coverImageURLString: "https://zhgchg.li/cover/3.png"),
SongModel(id: "3", name: "Lonely Together", user: UserModel(id: "1", name: "Avicii", profileImageURLString: "https://zhgchg.li/profile/1.png"), coverImageURLString: "https://zhgchg.li/cover/1.png"),
SongModel(id: "4", name: "Heaven", user: UserModel(id: "1", name: "Avicii", profileImageURLString: "https://zhgchg.li/profile/1.png"), coverImageURLString: "https://zhgchg.li/cover/4.png"),
SongModel(id: "5", name: "S.O.S", user: UserModel(id: "1", name: "Avicii", profileImageURLString: "https://zhgchg.li/profile/1.png"), coverImageURLString: "https://zhgchg.li/cover/5.png")],
coverImageURLString: "https://zhgchg.li/playlist/1.png")
A Poorly Designed Approach
Let’s start with a very messy implementation that doesn’t consider architecture at all.
Stephen Chow — The God of Cookery
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
class ShareManager {
private let title: String
private let urlString: String
private let imageURLStrings: [String]
init(user: UserModel) {
self.title = "Hi, I want to share a great artist with you: \(user.name)."
self.urlString = "https://zhgchg.li/user/\(user.id)"
self.imageURLStrings = [user.profileImageURLString]
}
init(song: SongModel) {
self.title = "Hi, I want to share a great song I just heard: \(song.user.name)'s \(song.name)."
self.urlString = "https://zhgchg.li/user/\(song.user.id)/song/\(song.id)"
self.imageURLStrings = [song.coverImageURLString]
}
init(playlist: PlaylistModel) {
self.title = "Hi, I can't stop listening to this playlist: \(playlist.name)."
self.urlString = "https://zhgchg.li/user/\(playlist.user.id)/playlist/\(playlist.id)"
self.imageURLStrings = playlist.songs.map({ $0.coverImageURLString })
}
func shareToFacebook() {
// call Facebook share SDK...
print("Sharing to Facebook...")
print("[)](\(self.urlString))")
}
func shareToInstagram() {
// call Instagram share SDK...
print("Sharing to Instagram...")
print(self.imageURLStrings.joined(separator: ","))
}
func shareToLine() {
// call Line share SDK...
print("Sharing to Line...")
print("[\(self.title)](\(self.urlString))")
}
}
There’s not much to say; this is a completely unstructured approach where everything is mixed together. If we want to add a new sharing platform, change the sharing information for a platform, or add a new shareable model, we have to modify the ShareManager. Additionally, the design of imageURLStrings
was influenced by the need for multiple images when sharing a playlist on Instagram, which leads to a design that is reactive rather than proactive, polluting other types that don’t require multiple images.
A Slight Optimization
Let’s separate the logic a bit.
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
protocol Shareable {
func getShareText() -> String
func getShareURLString() -> String
func getShareImageURLStrings() -> [String]
}
extension UserModel: Shareable {
func getShareText() -> String {
return "Hi, I want to share a great artist with you: \(self.name)."
}
func getShareURLString() -> String {
return "https://zhgchg.li/user/\(self.id)"
}
func getShareImageURLStrings() -> [String] {
return [self.profileImageURLString]
}
}
extension SongModel: Shareable {
func getShareText() -> String {
return "Hi, I want to share a great song I just heard: \(self.user.name)'s \(self.name)."
}
func getShareURLString() -> String {
return "https://zhgchg.li/user/\(self.user.id)/song/\(self.id)"
}
func getShareImageURLStrings() -> [String] {
return [self.coverImageURLString]
}
}
extension PlaylistModel: Shareable {
func getShareText() -> String {
return "Hi, I can't stop listening to this playlist: \(self.name)."
}
func getShareURLString() -> String {
return "https://zhgchg.li/user/\(self.user.id)/playlist/\(self.id)"
}
func getShareImageURLStrings() -> [String] {
return [self.coverImageURLString]
}
}
protocol ShareManagerProtocol {
var model: Shareable { get }
init(model: Shareable)
func share()
}
class FacebookShare: ShareManagerProtocol {
let model: Shareable
required init(model: Shareable) {
self.model = model
}
func share() {
// call Facebook share SDK...
print("Sharing to Facebook...")
print("[.first))](\(model.getShareURLString())")
}
}
class InstagramShare: ShareManagerProtocol {
let model: Shareable
required init(model: Shareable) {
self.model = model
}
func share() {
// call Instagram share SDK...
print("Sharing to Instagram...")
print(model.getShareImageURLStrings().joined(separator: ","))
}
}
class LineShare: ShareManagerProtocol {
let model: Shareable
required init(model: Shareable) {
self.model = model
}
func share() {
// call Line share SDK...
print("Sharing to Line...")
print("[\(model.getShareText())](\(model.getShareURLString())")
}
}
We extracted a Shareable
protocol, allowing any model that conforms to this protocol to support sharing. The sharing logic is also separated into a ShareManagerProtocol
, so adding new sharing options or modifying existing ones won’t affect other share managers.
However, the getShareImageURLStrings
method still feels odd. Additionally, if a new sharing platform requires entirely different model data (like WeChat needing play counts or creation dates), it could lead to confusion.
Using the Visitor Pattern
Here’s how we can solve this using the Visitor Pattern.
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Visitor Version
protocol Shareable {
func accept(visitor: SharePolicy)
}
extension UserModel: Shareable {
func accept(visitor: SharePolicy) {
visitor.visit(model: self)
}
}
extension SongModel: Shareable {
func accept(visitor: SharePolicy) {
visitor.visit(model: self)
}
}
extension PlaylistModel: Shareable {
func accept(visitor: SharePolicy) {
visitor.visit(model: self)
}
}
protocol SharePolicy {
func visit(model: UserModel)
func visit(model: SongModel)
func visit(model: PlaylistModel)
}
class ShareToFacebookVisitor: SharePolicy {
func visit(model: UserModel) {
// call Facebook share SDK...
print("Sharing to Facebook...")
print("[](https://zhgchg.li/user/\(model.id)")
}
func visit(model: SongModel) {
// call Facebook share SDK...
print("Sharing to Facebook...")
print("[)](https://zhgchg.li/user/\(model.user.id)/song/\(model.id)")
}
func visit(model: PlaylistModel) {
// call Facebook share SDK...
print("Sharing to Facebook...")
print("[)](https://zhgchg.li/user/\(model.user.id)/playlist/\(model.id)")
}
}
class ShareToLineVisitor: SharePolicy {
func visit(model: UserModel) {
// call Line share SDK...
print("Sharing to Line...")
print("[Hi, I want to share a great artist with you: \(model.name).](https://zhgchg.li/user/\(model.id)")
}
func visit(model: SongModel) {
// call Line share SDK...
print("Sharing to Line...")
print("[Hi, I want to share a great song I just heard: \(model.user.name)'s \(model.name).](https://zhgchg.li/user/\(model.user.id)/song/\(model.id)")
}
func visit(model: PlaylistModel) {
// call Line share SDK...
print("Sharing to Line...")
print("[Hi, I can't stop listening to this playlist: \(model.name).](https://zhgchg.li/user/\(model.user.id)/playlist/\(model.id)")
}
}
class ShareToInstagramVisitor: SharePolicy {
func visit(model: UserModel) {
// call Instagram share SDK...
print("Sharing to Instagram...")
print(model.profileImageURLString)
}
func visit(model: SongModel) {
// call Instagram share SDK...
print("Sharing to Instagram...")
print(model.coverImageURLString)
}
func visit(model: PlaylistModel) {
// call Instagram share SDK...
print("Sharing to Instagram...")
print(model.songs.map({ $0.coverImageURLString }).joined(separator: ","))
}
}
// Use case
let shareToInstagramVisitor = ShareToInstagramVisitor()
user.accept(visitor: shareToInstagramVisitor)
playlist.accept(visitor: shareToInstagramVisitor)
Let’s break down what we’ve done:
- We created a
Shareable
protocol to manage models that support sharing, providing a unified interface (though it could be left undefined). - The
UserModel
,SongModel
, andPlaylistModel
implement theaccept(visitor: SharePolicy)
method, so if we add a new model that supports sharing, we only need to implement this protocol. - We defined the
SharePolicy
protocol, listing the supported models. (It must be a concrete type; you might wonder why we don’t define it asvisit(model: Shareable)
. If we did, we’d fall back into the previous issues.) - Each sharing method implements the
SharePolicy
, combining the necessary resources based on the source. - If we were to add a WeChat sharing option that requires different data (like play counts or creation dates), it wouldn’t affect the existing code because it can retrieve the information it needs from the concrete model.
This achieves the goal of low coupling and high cohesion in software development.
The above is a classic implementation of the Visitor Double Dispatch pattern. However, in everyday development, we might encounter this situation less frequently; typically, there may only be one Visitor. I believe this pattern is also suitable for combining scenarios. For example, if we have a requirement to save to Core Data, we can directly define accept(visitor: SaveToCoreDataVisitor)
without needing to declare a separate Policy Protocol, which is also a good architectural approach.
1
2
3
protocol Saveable {
func accept(visitor: SaveToCoreDataVisitor)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class SaveToCoreDataVisitor {
func visit(model: UserModel) {
// Map UserModel to Core Data
}
func visit(model: SongModel) {
// Map SongModel to Core Data
}
func visit(model: PlaylistModel) {
// Map PlaylistModel to Core Data
}
}
Other applications: Save, Like, table view/collection view cell for row, etc.
Principles
Let’s discuss some common principles:
- Code is meant to be read by people; avoid over-designing.
- Consistency is crucial; the same codebase should use the same architectural methods in similar scenarios.
- If the scope is manageable or unlikely to encounter other situations, continuing to break things down further can be considered over-designing.
- Focus on applying existing solutions rather than inventing new ones; design patterns have been established in software design for decades and are likely to cover more scenarios than a new architecture we create.
- If you don’t understand a design pattern, it’s worth learning. However, if you create your own architecture, it may be harder to convince others to adopt it, as it might only apply to a specific case and not be common knowledge.
- Code duplication isn’t inherently bad; overly pursuing encapsulation can lead to over-design. Remember, code should be readable, and good code is characterized by low coupling and high cohesion.
- Avoid modifying patterns arbitrarily; there are reasons behind their design, and random changes can lead to issues in certain scenarios.
- Once you start deviating from a clear path, it becomes increasingly difficult to return, and the code can become messier.
inspired by @saiday
References
- Design Patterns in Swift: Visitor (Another application of the Visitor pattern)
- https://github.com/kingreza/Swift-Visitor
- Deep Linking at Scale on iOS (State Pattern)
Further Reading
- Practical applications of Design Patterns — In WKWebView with Builder, Strategy & Chain of Responsibility Pattern
- Practical applications of Design Patterns
- Visitor Pattern in TableView
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.