Swift Design Patterns
Introduction
Design patterns are reusable solutions to common problems that arise during software development. They represent best practices evolved over time by experienced developers. In Swift and iOS development, understanding these patterns can help you write more maintainable, flexible, and robust code.
This guide will introduce you to several essential design patterns in Swift, explain when and why you should use them, and provide practical examples of each.
Why Design Patterns Matter
Before diving into specific patterns, it's important to understand their benefits:
- They provide proven solutions to common problems
 - They make your code more reusable and maintainable
 - They establish a common vocabulary among developers
 - They help you write code that's easier to understand and debug
 - They encourage best practices and principles like SOLID
 
Common Design Patterns in Swift
Let's explore some of the most commonly used design patterns in Swift development.
1. MVC (Model-View-Controller)
MVC is the foundation design pattern for iOS development and separates your application into three main components:
- Model: The data and business logic
 - View: The user interface elements
 - Controller: The mediator that connects the model and view
 
Example Implementation
// Model
struct User {
    let name: String
    let email: String
}
// View
class UserProfileView: UIView {
    let nameLabel = UILabel()
    let emailLabel = UILabel()
    
    func configure(with user: User) {
        nameLabel.text = user.name
        emailLabel.text = user.email
    }
}
// Controller
class UserProfileViewController: UIViewController {
    let userProfileView = UserProfileView()
    var user: User?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(userProfileView)
        
        // Fetch user data and update view
        if let user = user {
            userProfileView.configure(with: user)
        }
    }
}
MVC is simple to understand but can lead to "Massive View Controllers" where too much logic ends up in the controller. This has led to the popularity of alternative patterns like MVVM.
2. MVVM (Model-View-ViewModel)
MVVM adds a ViewModel layer that handles the presentation logic:
- Model: The data and business logic
 - View: The user interface (includes UIViewControllers in iOS)
 - ViewModel: Transforms model information into values that can be displayed by the view
 
Example Implementation
// Model
struct User {
    let firstName: String
    let lastName: String
    let email: String
}
// ViewModel
class UserViewModel {
    private let user: User
    
    init(user: User) {
        self.user = user
    }
    
    var fullName: String {
        return "\(user.firstName) \(user.lastName)"
    }
    
    var email: String {
        return user.email
    }
}
// View (ViewController)
class ProfileViewController: UIViewController {
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var emailLabel: UILabel!
    
    var viewModel: UserViewModel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        updateUI()
    }
    
    func updateUI() {
        nameLabel.text = viewModel.fullName
        emailLabel.text = viewModel.email
    }
}
MVVM works especially well with reactive programming frameworks like Combine or RxSwift, where the view can automatically update when the ViewModel changes.
3. Singleton Pattern
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. It's commonly used for services, managers, or resources that should be shared across the app.
Example Implementation
class NetworkManager {
    // Shared instance
    static let shared = NetworkManager()
    
    // Private initializer prevents external instantiation
    private init() {}
    
    func fetchData(from url: URL, completion: @escaping (Data?, Error?) -> Void) {
        URLSession.shared.dataTask(with: url) { data, response, error in
            completion(data, error)
        }.resume()
    }
}
// Usage
let url = URL(string: "https://api.example.com/data")!
NetworkManager.shared.fetchData(from: url) { data, error in
    if let data = data {
        // Process the data
        print("Received \(data.count) bytes")
    } else if let error = error {
        print("Error: \(error.localizedDescription)")
    }
}
Note: While convenient, singletons should be used sparingly as they can make your code harder to test and maintain. Consider dependency injection as an alternative.
4. Factory Pattern
The Factory pattern provides an interface for creating objects without specifying their concrete classes. This is useful when object creation logic is complex or when you need to create different objects based on conditions.
Example Implementation
// Protocol for all payment methods
protocol PaymentMethod {
    func processPayment(amount: Double)
}
// Concrete implementations
class CreditCardPayment: PaymentMethod {
    func processPayment(amount: Double) {
        print("Processing credit card payment of $\(amount)")
    }
}
class PayPalPayment: PaymentMethod {
    func processPayment(amount: Double) {
        print("Processing PayPal payment of $\(amount)")
    }
}
class ApplePayPayment: PaymentMethod {
    func processPayment(amount: Double) {
        print("Processing Apple Pay payment of $\(amount)")
    }
}
// Factory class
class PaymentFactory {
    enum PaymentType {
        case creditCard, payPal, applePay
    }
    
    static func createPayment(of type: PaymentType) -> PaymentMethod {
        switch type {
        case .creditCard:
            return CreditCardPayment()
        case .payPal:
            return PayPalPayment()
        case .applePay:
            return ApplePayPayment()
        }
    }
}
// Usage
let paymentMethod = PaymentFactory.createPayment(of: .applePay)
paymentMethod.processPayment(amount: 99.99)
// Output: Processing Apple Pay payment of $99.99
The Factory pattern makes the code more maintainable and extensible when new payment methods need to be added.
5. Observer Pattern
The Observer pattern defines a one-to-many dependency between objects, so that when one object changes state, all its dependents are notified and updated automatically. In Swift, this can be implemented using various approaches.
Example with NotificationCenter
// Define notification name
extension Notification.Name {
    static let userDidUpdate = Notification.Name("userDidUpdate")
}
// Publisher
class UserManager {
    var currentUser: User? {
        didSet {
            // Post notification when user changes
            NotificationCenter.default.post(name: .userDidUpdate, object: currentUser)
        }
    }
}
// Observer
class ProfileViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Register for notifications
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(userDidUpdate),
            name: .userDidUpdate,
            object: nil
        )
    }
    
    @objc func userDidUpdate(notification: Notification) {
        if let user = notification.object as? User {
            // Update UI with user data
            print("User updated: \(user.name)")
        }
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}
Example with Combine (iOS 13+)
import Combine
class UserManager {
    // Subject to publish updates
    let userPublisher = PassthroughSubject<User, Never>()
    
    func updateUser(_ user: User) {
        userPublisher.send(user)
    }
}
class ProfileViewController: UIViewController {
    private var cancellables = Set<AnyCancellable>()
    let userManager = UserManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Subscribe to updates
        userManager.userPublisher
            .sink { [weak self] user in
                // Update UI with user data
                print("User updated: \(user.name)")
            }
            .store(in: &cancellables)
    }
}
The Observer pattern is particularly useful in iOS development for maintaining synchronized state across components.
6. Delegate Pattern
The Delegate pattern enables one object to send messages to another object when a specific event happens. It's widely used in iOS development, especially with UIKit components.
Example Implementation
// Define the delegate protocol
protocol TaskCompletionDelegate: AnyObject {
    func taskCompleted(result: String)
    func taskFailed(error: Error)
}
// Class that uses the delegate
class TaskManager {
    weak var delegate: TaskCompletionDelegate?
    
    func performTask() {
        // Simulate some work
        DispatchQueue.global().async {
            // Simulate success
            if Bool.random() {
                DispatchQueue.main.async {
                    self.delegate?.taskCompleted(result: "Task successful")
                }
            } else {
                // Simulate failure
                DispatchQueue.main.async {
                    let error = NSError(domain: "TaskError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Task failed"])
                    self.delegate?.taskFailed(error: error)
                }
            }
        }
    }
}
// Class that implements the delegate
class TaskViewController: UIViewController, TaskCompletionDelegate {
    let taskManager = TaskManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        taskManager.delegate = self
        taskManager.performTask()
    }
    
    func taskCompleted(result: String) {
        print("Task completed: \(result)")
    }
    
    func taskFailed(error: Error) {
        print("Task failed: \(error.localizedDescription)")
    }
}
The delegate pattern provides a cleaner, more decoupled way for objects to communicate compared to direct method calls.
Real-World Application: Combining Patterns
In real-world applications, you'll often use multiple design patterns together. Let's see how we might structure a simple weather app:
// Model
struct WeatherData: Codable {
    let temperature: Double
    let humidity: Int
    let description: String
}
// ViewModel
class WeatherViewModel {
    private(set) var weatherData: WeatherData?
    let weatherDataPublisher = PassthroughSubject<WeatherData, Error>()
    
    func fetchWeather(for city: String) {
        WeatherService.shared.fetchWeather(for: city) { [weak self] result in
            switch result {
            case .success(let data):
                self?.weatherData = data
                self?.weatherDataPublisher.send(data)
            case .failure(let error):
                self?.weatherDataPublisher.send(completion: .failure(error))
            }
        }
    }
    
    var formattedTemperature: String {
        guard let temp = weatherData?.temperature else { return "N/A" }
        return "\(Int(temp))°C"
    }
    
    var formattedHumidity: String {
        guard let humidity = weatherData?.humidity else { return "N/A" }
        return "\(humidity)%"
    }
    
    var weatherDescription: String {
        return weatherData?.description ?? "Unknown"
    }
}
// Singleton Service
class WeatherService {
    static let shared = WeatherService()
    
    private init() {}
    
    func fetchWeather(for city: String, completion: @escaping (Result<WeatherData, Error>) -> Void) {
        // Simulate network request
        DispatchQueue.global().async {
            // In a real app, you would make an API call here
            let mockData = WeatherData(
                temperature: Double.random(in: 0...30),
                humidity: Int.random(in: 30...90),
                description: ["Sunny", "Cloudy", "Rainy", "Windy"].randomElement()!
            )
            
            DispatchQueue.main.async {
                completion(.success(mockData))
            }
        }
    }
}
// View Controller (combines MVVM and Observer patterns)
import UIKit
import Combine
class WeatherViewController: UIViewController {
    @IBOutlet weak var cityTextField: UITextField!
    @IBOutlet weak var temperatureLabel: UILabel!
    @IBOutlet weak var humidityLabel: UILabel!
    @IBOutlet weak var descriptionLabel: UILabel!
    
    let viewModel = WeatherViewModel()
    private var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupBindings()
    }
    
    func setupBindings() {
        viewModel.weatherDataPublisher
            .receive(on: DispatchQueue.main)
            .sink { completion in
                if case let .failure(error) = completion {
                    print("Error: \(error.localizedDescription)")
                }
            } receiveValue: { [weak self] _ in
                self?.updateUI()
            }
            .store(in: &cancellables)
    }
    
    @IBAction func searchButtonTapped(_ sender: UIButton) {
        guard let city = cityTextField.text, !city.isEmpty else { return }
        viewModel.fetchWeather(for: city)
    }
    
    private func updateUI() {
        temperatureLabel.text = viewModel.formattedTemperature
        humidityLabel.text = viewModel.formattedHumidity
        descriptionLabel.text = viewModel.weatherDescription
    }
}
This example combines:
- MVVM pattern for separating presentation logic
 - Singleton pattern for the shared service
 - Observer pattern (using Combine) for reactive updates
 - Delegation pattern (implicitly through text field's delegate)
 
Summary
Design patterns are powerful tools that can significantly improve your Swift code. We've covered several essential patterns:
- MVC: The traditional pattern for iOS development
 - MVVM: A more testable alternative to MVC that separates presentation logic
 - Singleton: Ensures a class has only one instance with global access
 - Factory: Creates objects without exposing creation logic
 - Observer: Notifies objects of state changes
 - Delegate: Enables objects to communicate through defined protocols
 
Remember that design patterns are guidelines, not strict rules. Choose the patterns that best solve your specific problems and don't be afraid to adapt them to your needs.
Additional Resources
To deepen your understanding of design patterns in Swift:
- Ray Wenderlich's Design Patterns in Swift
 - Apple's Human Interface Guidelines for understanding patterns in the context of iOS design
 - Swift by Sundell articles on design patterns
 
Exercises
- Practice MVC: Create a simple todo app using the MVC pattern
 - Implement MVVM: Convert an existing view controller to use MVVM architecture
 - Create a Factory: Build a UI component factory that can create different styles of buttons or labels
 - Combine Patterns: Design an app feature that uses at least three different design patterns working together
 
By practicing these patterns in your projects, you'll gain a better understanding of when and how to apply them effectively.
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!