Visitor Pattern in iOS|Swift Design Pattern Practical Applications
Explore how the Visitor Pattern solves complex object operations in iOS Swift projects, streamlining code maintenance and enhancing scalability for developers facing evolving app requirements.
This post was translated with AI assistance — let me know if anything sounds off!
Table of Contents
Visitor Pattern in Swift (Share Object to XXX Example)
Analysis of Practical Use Cases for the Visitor Pattern (Sharing Products, Songs, Articles… to Facebook, Line, LinkedIn… Scenarios)
Photo by Daniel McCullough
Preface
I have known about “Design Patterns” for over 10 years now, but I still can’t confidently say that I fully master them. I’ve always had a vague understanding and have gone through all the patterns several times, 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 is that techniques like PHP, Laravel, iOS, Swift, SwiftUI, and similar applications have relatively low learning barriers when switching between them. However, fundamentals such as algorithms, data structures, and design patterns belong to the core skills. Core skills and techniques complement each other, but techniques are easier to learn while core skills are harder to master. A person skilled in techniques may not have strong core skills, but someone with strong core skills can quickly learn techniques. Therefore, rather than just complementing each other, core skills are the foundation that, combined with techniques, make one invincible.
Find a Learning Method That Suits You
Based on my previous learning experience, I believe the best way for me to learn Design Patterns is to master a few patterns deeply first; focus on internalizing and flexibly applying them, while developing the intuition to judge which pattern fits which scenario. Then, gradually accumulate knowledge of new patterns until fully mastered. I think the best approach is to find practical scenarios and learn through application.
Learning Resources
Here are two recommended free learning resources:
Swift Playgrounds - An interactive app by Apple to learn Swift programming with fun exercises.
Ray Wenderlich Tutorials - A collection of free tutorials and articles on iOS development and design patterns including Visitor pattern.
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 through real iOS development scenarios. This article will also be written with the same approach.
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 has many places that effectively use Visitor to solve architectural problems. During this experience, I also grasped the core principles of Visitor. So, the first chapter is dedicated to it!
What is 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 first refer directly to refactoringguru’s explanation of Visitor.
iOS Practical Scenario — Sharing Feature
Assume we have the following Models: UserModel, SongModel, and PlaylistModel. Now, we want to implement a sharing feature that can share to Facebook, Line, and Instagram. Each Model needs to present different sharing messages, and each platform requires different data:
The combined scenario is shown in the above image. The first table displays the customized content for each Model, and the second table shows the data required by each sharing platform.
Especially Instagram requires multiple images when sharing a Playlist, which differs from the sources needed for other shares.
Define Model
First, 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")
The Approach Without Any Thought
Without considering architecture at all, first implement the dirtiest approach without any thought.
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 an amazing 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 mess all mixed together. If you want to add a new sharing platform, change the sharing info of a platform, or add a shareable Model, you have to modify ShareManager. Also, the design of imageURLStrings was made an array because Instagram requires a group of images when sharing playlists. This is a bit backwards, designing the architecture based on specific needs, which pollutes other types that don’t need image groups.
Optimize a bit
Separate the logic slightly.
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 a great 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, so any Model conforming to this protocol supports sharing; the sharing part is also separated into ShareManagerProtocol. For new sharing features, just implement the protocol, and modifications or deletions won’t affect other ShareManagers.
However, getShareImageURLStrings still behaves oddly. Moreover, if a newly added sharing platform requires a Model with vastly different data—for example, WeChat sharing needs play count, creation date, and other info unique to it—this will start to cause confusion.
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), it was played.](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 go through what was done line by line:
First, we created a Shareable protocol, mainly to manage models that support sharing and provide a unified interface for the Visitor (optional to define).
UserModel/SongModel/PlaylistModel implement Shareable
func accept(visitor: SharePolicy). Later, if new models supporting sharing are added, they only need to implement the protocol.Define SharePolicy listing the supported Models
(must be concrete type)You might wonder why not define it asvisit(model: Shareable). Instead, that would repeat the previous version’s problem.Each Share method implements SharePolicy, assembling the required resources according to the source.
Suppose we add WeChat sharing today, which requires special data (play count, creation date). It won’t affect existing code because it can get the information it needs from the concrete model.
Achieve the goal of low coupling and high cohesion in software development.
The above is a classic Visitor Double Dispatch implementation, 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 additional Policy Protocols, 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 humans; avoid over-designing.
Consistency is important; the same scenario within the same codebase should use the same architectural approach.
If the scope is controllable or no other situations can occur, continuing to break it down further can be considered Over Designed.
Use more applications, less invention; Design Patterns have been in the software design field for decades, and the scenarios they consider are definitely more complete than creating a new architecture from scratch.
If you don’t understand Design Patterns, you can learn them, but if it’s a framework you created yourself, it’s harder to convince others to learn it. Because even if they learn it, it might only apply to that specific case, so it won’t become common sense.
Code duplication does not necessarily mean bad code. Overemphasizing encapsulation can lead to overdesign. As mentioned earlier, code is meant to be read by humans, so as long as it is readable with low coupling and high cohesion, it is good code.
Do not arbitrarily alter the pattern. The original design always has its reasoning, and random changes may cause issues in certain scenarios.
Once you start taking detours, you’ll only go further off track, and the code will become messier.
inspired by @saiday
References
Design Patterns in Swift: Visitor (Another practical use case of Visitor)
Deep Linking at Scale on iOS (State Pattern)
Further Reading
Practical Application Record of Design Patterns (Encapsulating Socket.io)
Practical Application of Design Patterns (Encapsulating WKWebView)
For any questions or feedback, feel free to contact me.
This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.

{:target="_blank"}](/assets/ba5773a7bfea/1*rbswlsges8_oS3pNI1-WKA.webp)

