Post

Visitor Pattern in iOS (Swift)

An analysis of the practical application of the Visitor Design Pattern in iOS development

Visitor Pattern in iOS (Swift)

ℹ️ℹ️ℹ️ 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](https://unsplash.com/@d_mccullough?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText){:target="_blank"}

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:

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](https://refactoringguru.cn/design-patterns/visitor){:target="_blank"}

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

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.title)](\(String(describing: self.imageURLStrings.first))](\(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("[![\(model.getShareText())](\(String(describing: model.getShareImageURLStrings().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("[![Hi, I want to share a great artist with you: \(model.name).](\(model.profileImageURLString)](https://zhgchg.li/user/\(model.id)")
    }
    
    func visit(model: SongModel) {
        // call Facebook share SDK...
        print("Sharing to Facebook...")
        print("[![Hi, I want to share a great song I just heard: \(model.user.name)'s \(model.name).](\(model.coverImageURLString))](https://zhgchg.li/user/\(model.user.id)/song/\(model.id)")
    }
    
    func visit(model: PlaylistModel) {
        // call Facebook share SDK...
        print("Sharing to Facebook...")
        print("[![Hi, I can't stop listening to this playlist: \(model.name).](\(model.coverImageURLString))](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, and PlaylistModel implement the accept(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 as visit(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

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

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