Design Patterns in Swift: From Basics to Practice
Design Patterns are proven solutions to common development problems. In the world of Swift and the Apple ecosystem (iOS/macOS), knowing them becomes essential for creating maintainable, testable, and scalable code. In this article, we will break down key patterns, dividing them into three main categories: creational, structural, and behavioral. Each pattern will be illustrated with a working example in Swift.
Creational Patterns: Singleton, Factory Method, and Builder
Creational patterns handle the process of object creation, making it more flexible and independent of client code.
Singleton
Ensures that a class has only one instance and provides a global point of access to it. In iOS, it is often used for managing network sessions, databases (Core Data), or application settings.
class NetworkManager { static let shared = NetworkManager() private init() {} func fetchData(from url: URL) { // Request logic print("Fetching data from \\(url)") }}
// UsageNetworkManager.shared.fetchData(from: URL(string: "https://example.com")!)Factory Method
Defines an interface for creating an object but lets subclasses alter the type of created object. It is great for creating UI components depending on the iOS version or configuration.
protocol Button { func render()}
class iOSButton: Button { func render() { print("Rendering iOS button") }}
class macOSButton: Button { func render() { print("Rendering macOS button") }}
enum Platform { case iOS, macOS }
class ButtonFactory { static func createButton(for platform: Platform) -> Button { switch platform { case .iOS: return iOSButton() case .macOS: return macOSButton() } }}
// Usagelet button = ButtonFactory.createButton(for: .iOS)button.render()Builder
Allows step-by-step creation of complex objects. It is widely used for configuring requests (URLRequest) or complex UI settings.
class Pizza { var dough: String = "" var cheese: String = "" var toppings: [String] = []}
class PizzaBuilder { private var pizza = Pizza() func setDough(_ dough: String) -> PizzaBuilder { pizza.dough = dough return self } func setCheese(_ cheese: String) -> PizzaBuilder { pizza.cheese = cheese return self } func addTopping(_ topping: String) -> PizzaBuilder { pizza.toppings.append(topping) return self } func build() -> Pizza { return pizza }}
// Usagelet margherita = PizzaBuilder() .setDough("Thin") .setCheese("Mozzarella") .addTopping("Basil") .build()Structural Patterns: Adapter, Decorator, and Facade
Structural patterns simplify design by defining ways to combine objects and classes into larger structures.
Adapter
Allows objects with incompatible interfaces to work together. A classic example is integrating third-party libraries or working with legacy code.
// Old protocolprotocol OldPaymentSystem { func pay(amount: Double)}
// New protocolprotocol NewPaymentSystem { func processPayment(amount: Double, currency: String)}
// Adapterclass PaymentAdapter: NewPaymentSystem { private let oldSystem: OldPaymentSystem init(oldSystem: OldPaymentSystem) { self.oldSystem = oldSystem } func processPayment(amount: Double, currency: String) { // Currency conversion let convertedAmount = amount * 1.1 oldSy