Design Patterns in iOS Development

  • Singleton Pattern
  • Delegation Pattern
  • Factory Pattern
  • Observer Pattern
  • Facade Pattern
  • Decorator Pattern
  • Adapter Pattern

Let’s dive deep into the Singleton Pattern in detail.


protocol TaskDelegate {
    func completeTask()
}

class Student: TaskDelegate {
    func completeTask() {
        print("Task completed!")
    }
}

class Teacher {
    var delegate: TaskDelegate?

    func assignTask() {
        delegate?.completeTask()
    }
}

let teacher = Teacher()
let student = Student()
teacher.delegate = student
teacher.assignTask()  // Output: "Task completed!"
import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    let tableView = UITableView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Set the delegate and data source
        tableView.delegate = self
        tableView.dataSource = self
        
        // Register a basic UITableViewCell for reuse
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        
        // Add the table view to the view controller's view
        view.addSubview(tableView)
        tableView.frame = view.bounds
    }
    
    // UITableViewDataSource method: Number of rows in the section
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10 // Example: 10 rows
    }
    
    // UITableViewDataSource method: Cell for row at indexPath
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = "Row \(indexPath.row)"
        return cell
    }
    
    // UITableViewDelegate method: Row selection
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("Selected row at index \(indexPath.row)")
    }
}

The Factory Pattern involves creating an object through a special method (often called a factory method) rather than calling a constructor directly. This pattern is particularly useful when the creation process is complex, involves conditional logic, or when you want to decouple the code that uses the objects from the code that creates them

Imagine you have a program that needs to create different types of vehicles like cars, bikes, and trucks. Instead of writing specific code to create each type of vehicle, you can use a factory method to handle the creation process.

protocol Vehicle {
    func drive()
}

class Car: Vehicle {
    func drive() {
        print("Driving a car")
    }
}

class Bike: Vehicle {
    func drive() {
        print("Riding a bike")
    }
}

class Truck: Vehicle {
    func drive() {
        print("Driving a truck")
    }
}

class VehicleFactory {
    enum VehicleType {
        case car, bike, truck
    }
    
    static func createVehicle(type: VehicleType) -> Vehicle {
        switch type {
        case .car:
            return Car()
        case .bike:
            return Bike()
        case .truck:
            return Truck()
        }
    }
}

// Usage
let car = VehicleFactory.createVehicle(type: .car)
car.drive() // Output: "Driving a car"

let bike = VehicleFactory.createVehicle(type: .bike)
bike.drive() // Output: "Riding a bike"

In the Observer Pattern, the subject maintains a list of its observers and notifies them automatically of any state changes. The observers can subscribe or unsubscribe from the subject’s notifications as needed. This pattern is commonly used to implement distributed event-handling systems, where multiple objects need to be informed about changes in the state of another object.

Think of a Netflix subscription. When a new movie is published (subject), all the subscribers (observers) receive the new movie information. Subscribers can unsubscribe if they no longer wish to receive any updates.

protocol Observer {
    func update(subject: Subject)
}

class Subject {
    private var observers = [Observer]()
    
    var state: Int = { didSet { notifyObservers() } }
    
    func addObserver(_ observer: Observer) {
        observers.append(observer)
    }
    
    func removeObserver(_ observer: Observer) {
        observers = observers.filter { $0 !== observer }
    }
    
    private func notifyObservers() {
        observers.forEach { $0.update(subject: self) }
    }
}

class ConcreteObserver: Observer {
    func update(subject: Subject) {
        print("Observer notified. New state is \(subject.state)")
    }
}

// Usage
let subject = Subject()
let observer1 = ConcreteObserver()
let observer2 = ConcreteObserver()

subject.addObserver(observer1)
subject.addObserver(observer2)

subject.state = 1  // Both observers are notified
import SwiftUI

class UserSettings: ObservableObject {
    @Published var username: String = "Guest"
}

struct ContentView: View {
    @ObservedObject var settings = UserSettings()
    
    var body: some View {
        VStack {
            Text("Username: \(settings.username)")
            
            Button("Change Username") {
                settings.username = "NewUser"
            }
        }
    }
}

// Usage in a SwiftUI app:
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
import UIKit

class MyViewController: UIViewController {
    
    var myView: UIView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        myView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 100))
        myView.backgroundColor = .blue
        view.addSubview(myView)
        
        // Add observer for the "frame" property of myView
        myView.addObserver(self, forKeyPath: #keyPath(UIView.frame), options: [.new, .old], context: nil)
    }
    
    // Override method to handle changes
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == #keyPath(UIView.frame) {
            if let newValue = change?[.newKey] as? CGRect {
                print("Frame changed to \(newValue)")
            }
        }
    }
    
    // Always remove the observer when done
    deinit {
        myView.removeObserver(self, forKeyPath: #keyPath(UIView.frame))
    }
}

// Usage in a UIKit app
let viewController = MyViewController()
import AVFoundation
import UIKit

class VideoPlayerViewController: UIViewController {
    
    var player: AVPlayer!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Initialize AVPlayer
        if let url = URL(string: "https://www.example.com/video.mp4") {
            player = AVPlayer(url: url)
        }
        
        // Add observer for the "status" property of AVPlayer
        player.addObserver(self, forKeyPath: #keyPath(AVPlayer.status), options: [.new, .old], context: nil)
        
        // Play the video
        player.play()
    }
    
    // Override method to handle changes
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == #keyPath(AVPlayer.status) {
            if let statusNumber = change?[.newKey] as? NSNumber {
                let status = AVPlayer.Status(rawValue: statusNumber.intValue)
                switch status {
                case .readyToPlay:
                    print("Player is ready to play")
                case .failed:
                    print("Player failed to load")
                default:
                    print("Player status changed")
                }
            }
        }
    }
    
    // Always remove the observer when done
    deinit {
        player.removeObserver(self, forKeyPath: #keyPath(AVPlayer.status))
    }
}

// Usage in a UIKit app
let videoPlayerViewController = VideoPlayerViewController()

The Facade Pattern involves creating a single class or interface that provides a simple method or a set of methods to interact with a more complex subsystem. Instead of interacting with multiple classes directly, clients can interact with the facade, which handles the interactions with the subsystem on their behalf.

Imagine you are using a home theater system with several components like a DVD player, projector, lights, and speakers. Instead of turning on each device separately, a facade can provide a single method to set everything up for watching a movie.

class DVDPlayer {
    func on() { print("DVD Player is on") }
    func play() { print("Playing movie") }
}

class Projector {
    func on() { print("Projector is on") }
    func setInput(_ input: String) { print("Projector input set to \(input)") }
}

class Lights {
    func dim() { print("Lights are dimmed") }
}

class SoundSystem {
    func on() { print("Sound system is on") }
    func setVolume(_ level: Int) { print("Volume set to \(level)") }
}

// Facade
class HomeTheaterFacade {
    private let dvdPlayer = DVDPlayer()
    private let projector = Projector()
    private let lights = Lights()
    private let soundSystem = SoundSystem()
    
    func watchMovie() {
        print("Get ready to watch a movie...")
        lights.dim()
        projector.on()
        projector.setInput("DVD")
        soundSystem.on()
        soundSystem.setVolume(5)
        dvdPlayer.on()
        dvdPlayer.play()
    }
}

// Usage
let homeTheater = HomeTheaterFacade()
homeTheater.watchMovie()
// Output:
// Get ready to watch a movie...
// Lights are dimmed
// Projector is on
// Projector input set to DVD
// Sound system is on
// Volume set to 5
// DVD Player is on
// Playing movie

The Decorator Pattern is a structural design pattern that allows you to add new functionality to an object dynamically, without altering its structure. This pattern is useful when you want to extend the behavior of an class in a flexible and reusable way.

The Decorator Pattern involves a set of decorator classes that are used to wrap concrete components. Each decorator class extends the functionality of the component it wraps, allowing you to mix and match behaviors as needed

Imagine you are designing a coffee shop app. You start with a basic coffee and then add extra features like milk, sugar, or whipped cream. Instead of creating separate subclasses for each combination (e.g., CoffeeWithMilkAndSugar), you can use decorators to add these features dynamically.

protocol Coffee {
    func cost() -> Double
    func description() -> String
}

class SimpleCoffee: Coffee {
    func cost() -> Double {
        return 5.0
    }
    
    func description() -> String {
        return "Simple Coffee"
    }
}

class CoffeeDecorator: Coffee {
    private let decoratedCoffee: Coffee
    
    init(decoratedCoffee: Coffee) {
        self.decoratedCoffee = decoratedCoffee
    }
    
    func cost() -> Double {
        return decoratedCoffee.cost()
    }
    
    func description() -> String {
        return decoratedCoffee.description()
    }
}

class MilkDecorator: CoffeeDecorator {
    override func cost() -> Double {
        return super.cost() + 1.5
    }
    
    override func description() -> String {
        return super.description() + ", Milk"
    }
}

class SugarDecorator: CoffeeDecorator {
    override func cost() -> Double {
        return super.cost() + 0.5
    }
    
    override func description() -> String {
        return super.description() + ", Sugar"
    }
}

// Usage
var coffee: Coffee = SimpleCoffee()
print(coffee.description())  // Output: "Simple Coffee"
print(coffee.cost())          // Output: 5.0

coffee = MilkDecorator(decoratedCoffee: coffee)
print(coffee.description())  // Output: "Simple Coffee, Milk"
print(coffee.cost())          // Output: 6.5

coffee = SugarDecorator(decoratedCoffee: coffee)
print(coffee.description())  // Output: "Simple Coffee, Milk, Sugar"
print(coffee.cost())          // Output: 7.0

The Adapter Pattern is a structural design pattern that allows incompatible interfaces to work together. It acts as a bridge between two incompatible classes by converting the interface of a class into another interface that the client expects. This pattern is particularly useful when you need to integrate a class that doesn’t fit with the existing code structure.

The Adapter Pattern involves creating an adapter class that wraps an existing class and provides a different interface to make it compatible with other classes. This adapter class translates the interface of the wrapped class into an interface that the client code can work with.

Imagine you have an old media player that only plays MP3 files, but now you want it to play MP4 videos as well. Instead of modifying the media player directly, you can create an adapter that allows the player to handle MP4 files using its existing methods.

// Old MediaPlayer class that only plays MP3
class MediaPlayer {
    func playMP3(fileName: String) {
        print("Playing MP3 file: \(fileName)")
    }
}

// New VideoPlayer class that plays MP4
class VideoPlayer {
    func playMP4(fileName: String) {
        print("Playing MP4 video: \(fileName)")
    }
}

// Adapter that makes VideoPlayer compatible with MediaPlayer interface
class MediaPlayerAdapter: MediaPlayer {
    private let videoPlayer: VideoPlayer
    
    init(videoPlayer: VideoPlayer) {
        self.videoPlayer = videoPlayer
    }
    
    override func playMP3(fileName: String) {
        // Using the adapter to play an MP4 file
        videoPlayer.playMP4(fileName: fileName)
    }
}

// Usage
let oldPlayer = MediaPlayer()
oldPlayer.playMP3(fileName: "song.mp3") // Output: "Playing MP3 file: song.mp3"

let videoPlayer = VideoPlayer()
let adapter = MediaPlayerAdapter(videoPlayer: videoPlayer)
adapter.playMP3(fileName: "movie.mp4") 

Stay Ahead of the Curve—Follow Us on Twitter!

1 thought on “Design Patterns in iOS Development”

Leave a Comment