Swift Delegation Pattern
The delegation pattern is one of the most commonly used design patterns in iOS and Swift development. If you've worked with UIKit components like table views or text fields, you've already experienced delegation in action! In this tutorial, we'll explore what the delegation pattern is, how it works in Swift, and how to implement it in your own code.
What is the Delegation Pattern?
Delegation is a design pattern that enables an object to hand off (or "delegate") some of its responsibilities to another object. Instead of an object handling all of its tasks internally, it can pass certain responsibilities to its delegate.
Think of delegation like asking a friend for help:
- You (the delegating object) need help with a task
- Your friend (the delegate) agrees to help by implementing specific methods
- When the task needs to be done, you call your friend to handle it
In Swift, this relationship is typically formalized through protocols, making delegation a clear, type-safe way for objects to communicate.
Why Use Delegation?
Delegation offers several advantages:
- Loose coupling: Objects don't need to know each other's specific types
- Reusability: Delegating objects can be reused with different delegate implementations
- Separation of concerns: Each object focuses on what it does best
- Customizability: The behavior of an object can be customized without subclassing
Basic Delegation Implementation
Let's walk through creating a simple delegation implementation:
Step 1: Define the Delegate Protocol
First, we define what responsibilities the delegate should handle:
protocol CoffeeBrewerDelegate: AnyObject {
func coffeeIsBrewed(amount: Int)
func coffeeBrewerDidRunOutOfWater()
}
Note the AnyObject
conformance, which restricts this protocol to class types only. This is typical for delegate protocols since we usually want to use weak references to avoid retain cycles.
Step 2: Create the Delegating Class
Next, we create the class that will delegate responsibilities:
class CoffeeBrewer {
weak var delegate: CoffeeBrewerDelegate?
private var waterLevel = 100
func brewCoffee(amount: Int) {
print("Starting to brew \(amount)ml of coffee...")
guard waterLevel >= amount else {
delegate?.coffeeBrewerDidRunOutOfWater()
return
}
// Simulate brewing
waterLevel -= amount
print("Coffee brewing complete. Water remaining: \(waterLevel)ml")
delegate?.coffeeIsBrewed(amount: amount)
}
func refillWater() {
waterLevel = 100
print("Water tank refilled. Current level: \(waterLevel)ml")
}
}
Notice we declare the delegate property as weak
to prevent a retain cycle. The CoffeeBrewer
calls the delegate methods when specific events occur.
Step 3: Implement the Delegate
Now let's create a class that implements the delegate protocol:
class CoffeeShop: CoffeeBrewerDelegate {
let brewer = CoffeeBrewer()
var coffeeServed = 0
init() {
brewer.delegate = self
}
func orderCoffee(size: Int) {
print("Coffee order received for \(size)ml")
brewer.brewCoffee(amount: size)
}
// MARK: - CoffeeBrewerDelegate methods
func coffeeIsBrewed(amount: Int) {
coffeeServed += amount
print("☕ Coffee is ready! Total served today: \(coffeeServed)ml")
}
func coffeeBrewerDidRunOutOfWater() {
print("⚠️ Coffee machine is out of water! Refilling...")
brewer.refillWater()
// Retry the last operation if needed
}
}
Step 4: Using Our Implementation
Let's see our delegation pattern in action:
// Create our coffee shop
let shop = CoffeeShop()
// Serve some coffee
shop.orderCoffee(size: 30)
shop.orderCoffee(size: 40)
shop.orderCoffee(size: 35)
Output:
Coffee order received for 30ml
Starting to brew 30ml of coffee...
Coffee brewing complete. Water remaining: 70ml
☕ Coffee is ready! Total served today: 30ml
Coffee order received for 40ml
Starting to brew 40ml of coffee...
Coffee brewing complete. Water remaining: 30ml
☕ Coffee is ready! Total served today: 70ml
Coffee order received for 35ml
Starting to brew 35ml of coffee...
⚠️ Coffee machine is out of water! Refilling...
Water tank refilled. Current level: 100ml
Delegation in UIKit
If you've worked with iOS development, you've already encountered delegation. UIKit uses delegation extensively. Let's look at a familiar example:
class ContactsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet private weak var tableView: UITableView!
private var contacts = ["Alice", "Bob", "Charlie", "David"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
}
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return contacts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath)
cell.textLabel?.text = contacts[indexPath.row]
return cell
}
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let contact = contacts[indexPath.row]
print("Selected contact: \(contact)")
}
}
Here, UITableView
delegates data management and user interaction handling to our view controller. The view controller doesn't need to be a specific subclass - it just needs to implement the required protocols.
Creating a Custom Alert System with Delegation
Let's build a more complex example: a custom alert system that uses delegation.
Step 1: Define the Alert Delegate Protocol
protocol CustomAlertDelegate: AnyObject {
func alertDidConfirm(withID id: String)
func alertDidCancel(withID id: String)
func alertDidAppear(withID id: String)
}
// Optional protocol extension for default implementations
extension CustomAlertDelegate {
func alertDidAppear(withID id: String) {
// Default empty implementation
}
}
Step 2: Create the Alert Class
class CustomAlert {
weak var delegate: CustomAlertDelegate?
let alertID: String
let title: String
let message: String
init(id: String, title: String, message: String) {
self.alertID = id
self.title = title
self.message = message
}
func show() {
print("---------------------------------------")
print("| \(title)")
print("| \(message)")
print("| [Confirm] [Cancel]")
print("---------------------------------------")
delegate?.alertDidAppear(withID: alertID)
// For this example, we'll simulate user actions
simulateUserAction()
}
private func simulateUserAction() {
// Randomly choose between confirm and cancel
if Bool.random() {
print("User tapped: Confirm")
delegate?.alertDidConfirm(withID: alertID)
} else {
print("User tapped: Cancel")
delegate?.alertDidCancel(withID: alertID)
}
}
}
Step 3: Implement the Alert Manager
class AlertManager: CustomAlertDelegate {
private var pendingActions: [String: () -> Void] = [:]
func showAlert(title: String, message: String, onConfirm: @escaping () -> Void) {
let alertID = UUID().uuidString
let alert = CustomAlert(id: alertID, title: title, message: message)
alert.delegate = self
// Store the completion handler for when user confirms
pendingActions[alertID] = onConfirm
alert.show()
}
// MARK: - CustomAlertDelegate
func alertDidConfirm(withID id: String) {
print("Alert with ID \(id) was confirmed")
// Execute the stored action
if let action = pendingActions[id] {
action()
}
// Clean up
pendingActions.removeValue(forKey: id)
}
func alertDidCancel(withID id: String) {
print("Alert with ID \(id) was cancelled")
// Clean up
pendingActions.removeValue(forKey: id)
}
func alertDidAppear(withID id: String) {
print("Alert with ID \(id) has appeared on screen")
}
}
Step 4: Using Our Alert System
let alertManager = AlertManager()
// Show some alerts
alertManager.showAlert(title: "Update Available",
message: "A new version is available. Update now?") {
print("Performing update...")
}
alertManager.showAlert(title: "Delete Item",
message: "Are you sure you want to delete this item?") {
print("Item deleted successfully.")
}
Possible Output:
---------------------------------------
| Update Available
| A new version is available. Update now?
| [Confirm] [Cancel]
---------------------------------------
Alert with ID 1A2B3C4D has appeared on screen
User tapped: Confirm
Alert with ID 1A2B3C4D was confirmed
Performing update...
---------------------------------------
| Delete Item
| Are you sure you want to delete this item?
| [Confirm] [Cancel]
---------------------------------------
Alert with ID 5E6F7G8H has appeared on screen
User tapped: Cancel
Alert with ID 5E6F7G8H was cancelled
Common Delegation Pitfalls and Best Practices
1. Retain Cycles
Always use weak
references for delegates to avoid retain cycles:
// Correct
weak var delegate: MyDelegate?
// Incorrect - may cause memory leaks
var delegate: MyDelegate?
2. Protocol Requirements
Be thoughtful about which methods are required versus optional:
protocol DetailedDelegate: AnyObject {
// Required
func requiredMethod()
// Optional methods defined in an extension
}
extension DetailedDelegate {
func optionalMethod() {
// Default implementation
}
}
3. Naming Conventions
Follow standard naming conventions for delegate methods:
protocol LoginManagerDelegate: AnyObject {
// Indicate that something did happen
func loginManagerDidLogin(_ manager: LoginManager, user: User)
// Indicate that something will happen, allowing prevention
func loginManager(_ manager: LoginManager, shouldLoginUser username: String) -> Bool
}
4. Multiple Delegates
If you need multiple objects to receive delegate callbacks, consider using the observer pattern or NotificationCenter instead of delegation.
Summary
The delegation pattern is a powerful technique in Swift for creating loosely coupled, reusable code. By using protocols to define responsibilities that can be implemented by any compatible type, you create code that's more maintainable and adaptable.
Key takeaways:
- Delegation uses protocols to define responsibilities that can be delegated
- It enables loose coupling between objects
- Use
weak
delegate properties to avoid retain cycles - The pattern is extensively used in Apple frameworks
Additional Resources
Exercises
- Create a simple weather app that uses delegation to update a view controller when the weather data is fetched
- Implement a custom text input component with a delegate that notifies when the text changes
- Refactor an existing project to use delegation instead of direct method calls between objects
- Design a delegation-based system for a simple game where game events are reported to a statistics tracker
Happy delegating!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)