- Singleton Pattern
- Delegation Pattern
- Factory Pattern
- Observer Pattern
- Facade Pattern
- Decorator Pattern
- Adapter Pattern
Let’s dive deep into the Singleton Pattern in detail.
Singleton Pattern:
For more details on the Singleton design pattern, please visit this link.
Delegation Pattern:
The Delegation pattern is a common and powerful design pattern in Swift that allows one object to communicate with or pass responsibility to another object. This pattern is widely used in iOS development, especially in UIKit, to manage interactions between different parts of an app
What is the Delegation Pattern in Swift?
The Delegation pattern involves two objects:
• Delegate: The object that implements the delegation methods.
• Delegator: The object that delegates responsibility to the delegate.
In simple terms, the delegator asks the delegate to do something or provide some information, and the delegate performs the task or provides the data.
Example:
Imagine a teacher (delegator) who assigns a task to a student (delegate). The student completes the task and reports back to the teacher.
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!"
Delegation pattern in UIKit
One of the most common examples of the Delegation pattern in UIKit is with UITableView. The UITableView uses delegation to handle various tasks like displaying data and responding to user interactions.
Example: UITableView and UITableViewDelegate
When you work with a UITableView, you usually set up a delegate and a data source. These are responsible for providing the data to the table and handling user interactions.
• Data Source (UITableViewDataSource): Provides the data that the table view will display, like the number of rows and the content of each cell.
• Delegate (UITableViewDelegate): Handles user interactions with the table, like when a row is selected or when you want to customize the appearance of the cells.
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)")
}
}
Factory Pattern
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
Example:
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"
Why Use the Factory Pattern?
• Simplifies Object Creation: The Factory Pattern centralizes the creation logic in one place, making it easier to manage and modify.
• Reduces Code Duplication: Instead of writing repetitive code to create objects, you can reuse the factory method.
• Encapsulates Object Creation: By hiding the creation details, you make your code easier to maintain and extend.
When to Use the Factory Pattern?
• When the creation process is complex: If creating an object involves several steps or conditional logic, the Factory Pattern helps to keep your code clean and organized.
• When you need to manage different object types: If your application needs to create different types of related objects, a factory method can simplify the process.
• When you want to decouple the creation process: By using the Factory Pattern, you decouple the code that creates objects from the code that uses them, making your application more flexible and easier to change.
Observer Pattern
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.
Example:
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
Example of Observer Pattern in SwiftUI and UIKit
1. Observer Pattern in SwiftUI
In SwiftUI, the @Published property wrapper and @ObservedObject are commonly used to implement the Observer pattern. When a property marked with @Published changes, any view that observes this property using @ObservedObject will automatically update.
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()
}
}
}
2. Observer Pattern in UIKit
In UIKit, the NotificationCenter is a common way to implement the Observer pattern. It allows you to broadcast notifications to multiple observers.
and 2nd most commonly used Observer pattern is Key-Value Observing (KVO). KVO is a mechanism that allows objects to observe changes to properties of other objects. This pattern is heavily used in many UIKit components.
Example: UIViewController and KVO
UIViewController uses KVO to observe changes to its view property. For instance, if you want to observe changes to the frame property of a UIView, you can use KVO.
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()
3. Observer Pattern in AVPlayer
Another real-world example is using AVPlayer, a class in the AVFoundation framework that works with media playback. AVPlayer uses KVO to observe properties like status, rate, or currentItem.
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()
Why Use the Observer Pattern?
• Decoupling: The Observer Pattern decouples the subject and observers, meaning that the subject doesn’t need to know about the observers’ details. It only needs to notify them of changes.
• Flexibility: Observers can be added or removed dynamically. This means you can easily change which objects are notified of changes.
• Multiple Listeners: The Observer Pattern allows multiple objects to listen to changes in the subject simultaneously.
When to Use the Observer Pattern?
• Event Handling: Use this pattern when one object needs to notify multiple objects about an event or change in state.
• Data Binding: It’s commonly used in UI frameworks where the UI (observer) needs to react to changes in the underlying data model (subject).
• Real-time Notifications: Use the Observer Pattern in scenarios where you need to push updates to multiple components in real time.
Facade Pattern
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.
Example:
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
Why Use the Facade Pattern?
• Simplifies Complexity: The Facade Pattern hides the complexity of a system by providing a simple interface, making it easier for clients to use.
• Reduces Dependencies: Clients only interact with the facade, which reduces the number of dependencies they have on the underlying subsystem. This leads to better maintainability.
• Improves Code Readability: By providing a single point of interaction, the Facade Pattern makes the code more readable and easier to understand.
When to Use the Facade Pattern?
• When dealing with complex subsystems: Use the Facade Pattern when you have a system with many interdependent classes, and you want to provide a simpler way for clients to interact with it.
• To reduce coupling: Use it when you want to reduce the dependencies between clients and the underlying subsystems, making your codebase easier to maintain and modify.
• To improve code readability: The Facade Pattern can help make your codebase easier to understand by providing clear, high-level methods that encapsulate complex operations.
Decorator Pattern in Swift
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
Example:
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
Why Use the Decorator Pattern?
• Flexible Functionality: The Decorator Pattern allows you to add or remove features from an object at runtime without changing its structure.
• Composability: You can mix and match different decorators to create various combinations of features, making your code more modular and reusable.
• Single Responsibility Principle: Each decorator class has a single responsibility, making the code easier to maintain and extend.
When to Use the Decorator Pattern?
• When you want to add behavior dynamically: Use the Decorator Pattern when you need to add responsibilities to individual objects without affecting other objects of the same class.
• When subclassing would lead to too many classes: If adding features through subclassing would result in an explosion of subclasses, the Decorator Pattern provides a more manageable alternative.
• When you want to keep your codebase flexible and extensible: The Decorator Pattern allows you to add new functionality without altering existing code, making it easier to maintain and extend.
Adapter Pattern
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.
Example:
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")
Why Use the Adapter Pattern?
• Integrates Legacy Code: The Adapter Pattern is ideal for integrating new functionality into an existing codebase, especially when working with legacy systems that cannot be easily modified.
• Promotes Reusability: By adapting interfaces, you can reuse existing classes even if their interfaces don’t match the rest of the code.
• Decouples Code: The Adapter Pattern helps in decoupling client code from the implementations of the services it uses, making the system more flexible and easier to extend.
When to Use the Adapter Pattern?
• When you have incompatible interfaces: Use the Adapter Pattern when you need to integrate two classes that have incompatible interfaces.
• When you want to reuse existing code: If you have a class that is almost what you need but its interface is different, the Adapter Pattern allows you to reuse the class without modifying its source code.
• When integrating third-party libraries: If you’re using a third-party library with an interface that doesn’t match your needs, you can create an adapter to fit it into your application.
Stay Ahead of the Curve—Follow Us on Twitter!
1 thought on “Design Patterns in iOS Development”