Visitor Pattern in iOS|Swift Design Pattern Practical Applications
Explore how the Visitor Pattern solves complex object structure traversal in iOS Swift development, enhancing code maintainability and scalability with clear, real-world use cases and implementation strategies.
点击这里查看本文章简体中文版本。
點擊這裡查看本文章正體中文版本。
This post was translated with AI assistance — let me know if anything sounds off!
Visitor Pattern in Swift (Share Object to XXX Example)
Visitor Pattern Practical Application Scenario Analysis (Sharing Products, Songs, Articles… to Facebook, Line, LinkedIn… Scenarios)
Photo by Daniel McCullough
Introduction
“Design Pattern” has been known to me for over 10 years now, yet I still can’t confidently say I fully master it. I’ve always had only a vague understanding. Several times, I’ve gone through all the patterns from start to finish, but without internalizing or applying them in practice, I quickly forget them.
I’m really useless.
Internal Skills and Techniques
A great analogy I once saw compares skills like PHP, Laravel, iOS, Swift, SwiftUI, and similar applications to “techniques.” Switching between them has a relatively low learning curve. However, “internal skills” like algorithms, data structures, and design patterns form the core foundation. Internal skills and techniques complement each other, but techniques are easy to learn while internal skills are hard to master. Having strong techniques doesn’t guarantee strong internal skills, but with strong internal skills, you can quickly learn new techniques. So rather than just complementing each other, internal skills are the foundation, and combined with techniques, they make you unstoppable.
Find the Learning Method That Suits You
Based on my previous learning experience, the best way for me to learn Design Patterns is to focus deeply first and then broaden my understanding. I concentrate on mastering a few patterns thoroughly, internalizing and applying them flexibly, while developing an intuition to judge which pattern fits which scenario. Then, I gradually accumulate new patterns until I have mastered them all. I believe the best approach is to find practical situations and learn through application.
Learning Resources
Recommend two free learning resources
https://refactoringguru.cn/: A complete introduction to all pattern structures, scenarios, and relationships
https://shirazian.wordpress.com/2016/04/11/design-patterns-in-swift/: The author introduces the application of various patterns based on real iOS development scenarios. This article will also be written from this perspective.
Visitor — Behavioral Patterns
Chapter 1 records the Visitor Pattern, which is one of the treasures I discovered after working at StreetVoice for a year. The StreetVoice App uses Visitor extensively to solve architectural problems. During this experience, I grasped the core principles of the Visitor pattern. So, the first chapter is dedicated to it!
What is a Visitor
First, understand what Visitor is. What problem does it aim to solve? What is its structure?
Image taken from refactoringguru
The detailed content will not be repeated here. Please refer directly to refactoringguru’s explanation of Visitor.
iOS Practical Scenario — Sharing Feature
Suppose we have the following Models: UserModel, SongModel, and PlaylistModel. Now, we want to implement a sharing feature that supports Facebook, Line, and Instagram. Each Model needs to present a different sharing message, and each platform requires different data:
The combined scenario is shown in the image above. The first table displays the customized content for each model, and the second table shows the data required by each sharing platform.
Especially on Instagram, sharing a Playlist requires multiple images, which is different from other sharing sources.
Define Model
First, complete the property definitions 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")
Doing It Without Thinking
Completely ignoring the architecture, first implement the dirtiest approach without any consideration.
Stephen Chow — 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, sharing with you a great artist \(user.name)."
self.urlString = "https://zhgchg.li/user/\(user.id)"
self.imageURLStrings = [user.profileImageURLString]
}
init(song: SongModel) {
self.title = "Hi, sharing 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("Share to Facebook...")
print("[)](\(self.urlString))")
}
func shareToInstagram() {
// call Instagram share sdk...
print("Share to Instagram...")
print(self.imageURLStrings.joined(separator: ","))
}
func shareToLine() {
// call Line share sdk...
print("Share to Line...")
print("[\(self.title)](\(self.urlString))")
}
}
Nothing much to say, it’s a zero-architecture all mixed together. If you want to add a new sharing platform, change the sharing info of a certain platform, or add a shareable model, you have to modify the ShareManager. Also, imageURLStrings was designed as an array because Instagram requires a set of images when sharing playlists. This is a bit backwards, as the architecture was designed based on specific needs, causing other types that don’t need image sets to be polluted.
Optimize it a bit
Slightly separate the logic.
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, sharing with you an awesome artist \(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, sharing 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("Share 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("Share 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("Share to Line...")
print("[\(model.getShareText())](\(model.getShareURLString())")
}
}
We extracted a CanShare Protocol; any Model conforming to this protocol supports sharing. The sharing part is also separated into ShareManagerProtocol. For new sharing features, just implement the protocol. Modifications or deletions won’t affect other ShareManagers.
But getShareImageURLStrings is still strange. Also, if the new sharing platform requires a Model with vastly different data—like WeChat sharing needing play count, creation date, and other info only it uses—things start to get messy.
Visitor
Using the Visitor Pattern solution.
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("Share to Facebook...")
print("[](https://zhgchg.li/user/\(model.id)")
}
func visit(model: SongModel) {
// call Facebook share sdk...
print("Share to Facebook...")
print("[)](https://zhgchg.li/user/\(model.user.id)/song/\(model.id)")
}
func visit(model: PlaylistModel) {
// call Facebook share sdk...
print("Share 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("Share to Line...")
print("[Hi sharing a great artist \(model.name) with you.](https://zhgchg.li/user/\(model.id)")
}
func visit(model: SongModel) {
// call Line share sdk...
print("Share to Line...")
print("[Hi sharing a great song I just heard, \(model.user.name)'s \(model.name), played this way.](https://zhgchg.li/user/\(model.user.id)/song/\(model.id)")
}
func visit(model: PlaylistModel) {
// call Line share sdk...
print("Share 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("Share to Instagram...")
print(model.profileImageURLString)
}
func visit(model: SongModel) {
// call Instagram share sdk...
print("Share to Instagram...")
print(model.coverImageURLString)
}
func visit(model: PlaylistModel) {
// call Instagram share sdk...
print("Share 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 review what was done line by line:
First, we created a Shareable protocol to help manage models that support sharing, providing a unified interface for the Visitor (optional to define).
UserModel/SongModel/PlaylistModel implement Shareable
func accept(visitor: SharePolicy)
. In the future, if new models support sharing, they only need to implement this protocol.Define SharePolicy listing the supported Models
(must be concrete type)
You might wonder why not define it asvisit(model: Shareable)
If so, it would repeat the problem from the previous version.Each Share method implements SharePolicy and assembles the required resources according to the source.
Assuming there is an additional WeChat share today, it requires special data (play count, creation date) and will not affect the existing code because it can get the needed information from the concrete model itself.
Achieve low coupling and high cohesion in program development.
The above is a classic implementation of Visitor Double Dispatch, but in daily development, we rarely encounter this situation. Usually, there is only one Visitor, but I think this pattern combination is still very suitable. For example, if there is a SaveToCoreData requirement, we can directly define accept(visitor: SaveToCoreDataVisitor)
without declaring a separate Policy Protocol, which is also a good architectural approach.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protocol Saveable {
func accept(visitor: SaveToCoreDataVisitor)
}
class SaveToCoreDataVisitor {
func visit(model: UserModel) {
// map UserModel to coredata
}
func visit(model: SongModel) {
// map SongModel to coredata
}
func visit(model: PlaylistModel) {
// map PlaylistModel to coredata
}
}
Other Applications: Save, Like, tableview/collectionview cellforrow….
Principles
Finally, let’s discuss some common principles.
Code is meant to be read by people; avoid over-designing.
Consistency is important. The same architecture and approach should be used for the same scenario within the same codebase.
If the scope is controllable or no other situations are likely to occur, continuing to break it down further can be considered Over Designed.
Use more applications, less invention; Design Patterns have been in software design for decades, and the scenarios they consider are definitely more complete than creating a new architecture from scratch.
Not understanding Design Patterns can be learned, but if it’s a self-created architecture, it’s harder to convince others to learn it because it might only apply to that specific case. It then isn’t considered common sense.
Code duplication does not necessarily mean bad code. Overemphasizing encapsulation can lead to over-design. As mentioned earlier, code is meant to be read by people, so as long as it is readable with low coupling and high cohesion, it is good code.
Do not arbitrarily modify the pattern. The design has its reasons, and random changes may cause issues in certain scenarios.
Once you start taking detours, you’ll go further and further off course, and the code will become messier and messier.
inspired by @saiday
References
Design Patterns in Swift: Visitor (Another use case for the Visitor pattern)
Deep Linking at Scale on iOS (State Pattern)
Further Reading
Practical Application Record of Design Patterns — In WKWebView with Builder, Strategy & Chain of Responsibility Pattern
If you have any questions or suggestions, feel free to contact me.
This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.