Waldo sessions now support scripting! – Learn more
App Development

Swift Design Patterns: 3 Must-Know Patterns With Code Examples

Juan Reyes
Juan Reyes
Swift Design Patterns: 3 Must-Know Patterns With Code Examples
June 14, 2022
9
min read

This article will lay down the three must-know design patterns with code examples for Swift development. If you don't know what a design pattern is, don't worry. Our exploration of the subject will include all the information you'll need to apply these patterns successfully.

First, we'll define what design patterns in programming are. Then we'll explain why they're crucial for creating and maintaining suitable code bases. Finally, we'll take a deep dive into the three most important design patterns and provide some code examples.

Keep in mind that this article will contain a lot of theory and not a significant amount of code. So, I hope you have some popcorn and a notebook handy.

What Are Design Patterns?

As the name implies, a design pattern is nothing more than a repeatable pattern that helps you create better code. It's that simple.

I know. You probably expected a more convoluted definition. But that's really what it boils down to.

Much like a template, a design pattern serves as a guide for programming that has been proven to be effective. But isn't that the same thing?

Well, no.

Unlike templates, code snippets, or project scaffolding, you can't merely paste a design pattern into your application. This is because a design pattern isn't a piece of code. Instead, it's a broad concept that helps you solve a problem or meet a requirement.

Much like a template, a design pattern serves as a guide for programming that has been proven to be effective.

Why Are Design Patterns Important?

Among the many benefits of design patterns, you can find the following:

  • Consolidation of code. Design patterns help minimize the amount of work and, by extension, the amount of code necessary. It achieves this by providing a proven structure that meets extensive requirements.
  • Standardization of process. Design patterns provide a standard baseline that helps developers understand the thought process and approach to a problem preceding a solution by simply stating which pattern was used.
  • Tested framework. Design patterns provide tested and optimized solutions, sparing you from common architectural design and development mistakes.

Types of Design Patterns in Swift

Design patterns can be classified into three distinctive types: creational, structural, and behavioral.

Let's explore them.

Creational design patterns

Patterns falling into the creational type revolve around mechanisms for object creation. These patterns focus on the particular situation for which you create objects. Some examples are the singleton design pattern, prototype design pattern, and builder design pattern.

Structural design patterns

In contrast, structural design patterns streamline the design of classes and structures. These patterns focus on finding a straightforward method for establishing relationships between objects and classes. Some examples are the adapter design pattern, the bridge design pattern, and the proxy design pattern.

Behavioral design patterns

Finally, behavioral design patterns determine common communication patterns between entities and apply them. Some examples are the observer design pattern, the template design pattern, and the chain of responsibility design pattern.

Design patterns can be classified into three distinctive types: creational, structural, and behavioral.

Top Three Swift Design Patterns

As you can see, some of these design patterns are common in the development world. And for good reason. Chances are that you were taught some of them or you internalized them by merely working and collaborating with other people. This is because they serve as a solid foundation for robust development and good practices that deliver reliable results.

Now, let's see the top three must-know design patterns for Swift.

Builder Design Pattern

The builder design pattern allows for creating complex objects one step at a time. Instead of creating monolithic objects with numerous parameters, dependencies, and configurations, builders reduce the scope to specific requirements so that an object can be more manageable and straightforward for different uses. This pattern saves a significant amount of time and effort when dealing with extensive libraries and large codebases containing objects that satisfy many requirements and features.

Let's illustrate.

An excellent example of this pattern is an object that requires gradual initialization of numerous properties and needs many nested objects. Generally, the initialization code lies under a constructor with many parameters or is spread all over the class. To solve this issue, the builder design pattern requires developers to isolate the object constructor in the class code. This constructor is then assigned to what is known as builders and split into multiple steps. Now, to instantiate the object, all you need to do is to call the necessary builders to satisfy the requirements.

The builder design pattern is handy when complex object composition is inevitable and when code must construct different views for specific objects.

Here's some simple sample code following the builder design pattern.


protocol CarShop {
    func buildCar()
}
class Toyota: CarShop {
    func buildCar() {
        print("New Toyota Built!")
    }
}
class Honda: CarShop {
    func buildCar() {
        print("New Honda Built!")
    }
}
class Builder {
    let carShop: CarShop
    init(carShop: CarShop) {
        self.carShop = carShop
    }
    func build() {
        carShop.buildCar()
    }
}
let toyotaBuilder = Builder(carShop: Toyota())
toyotaBuilder.build()
toyotaBuilder.build()
toyotaBuilder.build()
let hondaBuilder = Builder(carShop: Honda())
hondaBuilder.build()
hondaBuilder.build()

Notice that both the Toyota and Honda shop classes follow the 'CarShop' protocol, which defines the 'buildCar' function for the builder class. So by using the builder object and providing it with the 'carShop', it can then build cars.

In this case, if you need to add more complexity to either the Toyota or Honda shop classes, the constructor for these classes can stay isolated on the builder.

Adapter Design Pattern

The adapter design pattern allows objects that are otherwise incompatible to work together by serving as an intermediary, adapting it to the other object. The role of an adapter is to wrap an object and create a mechanism to translate or adapt its properties and behavior to the target object. To illustrate, an object that contains a currency like the dollar can be wrapped into an adapter that converts it into yen for an API that only works with that currency.

The adapter design pattern is handy when using a third-party class containing an incompatible interface and when using existing subclasses with some missing functionality.


import EventKit
protocol EventProtocol: AnyObject {
    var title: String { get }
    var from: String { get }
    var to: String { get }
}
class MyEventAdapter: EventProtocol {
    private var event: EKEvent
    private lazy var dateFormatter: DateFormatter = {
        let df = DateFormatter()
        df.dateFormat = "MM-dd-yyyy HH:mm"
        return df
    }()
    var title: String {
        return event.title
    }
    var from: String {
        return dateFormatter.string(from: event.startDate)
    }
    var to: String {
        return dateFormatter.string(from: event.endDate)
    }
    init(event: EKEvent) {
        self.event = event
    }
}
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm"
let eventStore = EKEventStore()
let event = EKEvent(eventStore: eventStore)
event.title = "Design Pattern Meetup"
event.startDate = dateFormatter.date(from: "2022/01/01 15:00")
event.endDate = dateFormatter.date(from: "2022/12/12 18:00")
let myAdapter = MyEventAdapter(event: event)
myAdapter.title
myAdapter.from
myAdapter.to

Notice how the protocol establishes the same properties between the 'EKEvent' and the 'MyEventAdapter' class and that this class then maps the properties to match the protocol.

Template Design Pattern

The template design pattern allows subclasses to redefine specific elements of an algorithm without changing its general structure. In essence, this design pattern defines a structure for algorithms and delegates responsibility to subclasses. It achieves this by sectioning an algorithm into a series of stages, representing them in different methods, and calling them sequentially. The template design pattern is handy when subclasses must extend an algorithm without modifying its base structure and when having multiple classes that are responsible for comparable actions.

Here's an example of the template pattern with the standard photo and camera permission snippet.


import AVFoundation
import Photos
typealias AuthorizationCompletion = (status: Bool, message: String)
class PermissionService: NSObject {
    private var message: String = ""
    func authorize(_ completion: @escaping (AuthorizationCompletion) -> Void) {
        let status = checkStatus()
        guard !status else {
            complete(with: status, completion)
            return
        }
        requestAuthorization { [weak self] status in
            self?.complete(with: status, completion)
        }
    }
    func checkStatus() -> Bool {
        return false
    }
    func requestAuthorization(_ completion: @escaping (Bool) -> Void) {
        completion(false)
    }
    func formMessage(with status: Bool) {
        let messagePrefix = status ? "You have access to " : "You haven't access to "
        let nameOfCurrentPermissionService = String(describing: type(of: self))
        let nameOfBasePermissionService = String(describing: type(of: PermissionService.self))
        let messageSuffix = nameOfCurrentPermissionService.components(separatedBy: nameOfBasePermissionService).first!
        message = messagePrefix + messageSuffix
    }
    private func complete(with status: Bool, _ completion: @escaping (AuthorizationCompletion) -> Void) {
        formMessage(with: status)
        let result: AuthorizationCompletion = (status: status, message: message)
        completion(result)
    }
}
class CameraPermissionService: PermissionService {
    override func checkStatus() -> Bool {
        let status = AVCaptureDevice.authorizationStatus(for: .video).rawValue
        return status == AVAuthorizationStatus.authorized.rawValue
    }
    override func requestAuthorization(_ completion: @escaping (Bool) -> Void) {
        AVCaptureDevice.requestAccess(for: .video) { status in
            completion(status)
        }
    }
}
class PhotoPermissionService: PermissionService {
    override func checkStatus() -> Bool {
        let status = PHPhotoLibrary.authorizationStatus().rawValue
        return status == PHAuthorizationStatus.authorized.rawValue
    }
    override func requestAuthorization(_ completion: @escaping (Bool) -> Void) {
        PHPhotoLibrary.requestAuthorization { status in
            completion(status.rawValue == PHAuthorizationStatus.authorized.rawValue)
        }
    }
}
let permissionServices = [CameraPermissionService(), PhotoPermissionService()]
for permissionService in permissionServices {
    permissionService.authorize { (_, message) in
        print(message)
    }
}

In this case, the mechanism to get permission to access the photos and camera is handled by two subclasses, CameraPermissionService and PhotoPermissionService. These classes redefine specific stages of the algorithm while maintaining the rest intact.

Moving On

The great thing about design patterns is that they exist to make it easier for you to write your code effectively. In addition, they help you create a simple and proven development environment that saves you tons of time and headaches, all while providing high-quality results.

However, it's essential to know that no code is immune to bugs and human error, even when using design patterns. That's why we encourage you to check out Waldos' zero-code testing solution.

You can learn more about it here.

Automated E2E tests for your mobile app

Waldo provides the best-in-class runtime for all your mobile testing needs.
Get true E2E testing in minutes, not months.

Reproduce, capture, and share bugs fast!

Waldo Sessions helps mobile teams reproduce bugs, while compiling detailed bug reports in real time.