Lately, I’ve been writing a lot about UIKit and I noticed I haven’t covered the delegate pattern.
This pattern is not exclusive to UIKit but it’s quite used in this framework.
In this post, we’ll dive into the Delegation Pattern, a fundamental design pattern in iOS development that helps in creating a well-structured codebase.
What is the Delegation Pattern?
The Delegation Pattern is a design pattern where one object (the delegator) hands off (or delegates) some of its responsibilities to another object (the delegate). This allows for a clear separation of concerns and makes it easier to manage and extend code.
By responsibilities, we mean functions or actions the object needs to perform but doesn’t want to implement itself because it’s not directly responsible. This allows for a clear separation of concerns and makes it easier to manage and extend code.
Why Use the Delegation Pattern?
- Separation of Concerns: It helps in dividing the functionality into different classes, making the code more modular and easier to manage.
- Reusability: Delegate objects can be reused across different contexts and components.
- Flexibility: It provides a way to customize or extend the behavior of a class without modifying its source code.
How to Implement the Delegation Pattern in Swift
Let’s go through a practical example to illustrate the Delegation Pattern.
Step 1: Define the Protocol
First, define a protocol that outlines the responsibilities to be delegated.
protocol CustomDelegate: AnyObject {
func someDelegatedFunction()
func someOtherDelegatedFunction(withString: String)
}
Here, we are specifying the actions our object needs to perform but are not implemented by the object itself.
We can also pass information back to the delegate object. This can be as simple as one string or as complex as the whole object.
Step 2: Create the Delegator
Next, create the delegator class, which will hold a reference to the delegate and call its methods when appropriate.
class SomeObject {
weak var delegate: CustomDelegate?
func someAction() {
delegate?.someDelegatedFunction()
}
func someOtherAction() {
delegate?.someOtherDelegatedFunction(withString: "Some data being passed")
}
}
It’s important to mark the delegate property as
weak
to avoid retain cycles and memory leaks.
This is a minimum example. In most cases, the object will also contain its own logic and only some parts will be delegated.
Step 3: Implement the Delegate
Finally, implement the delegate in a class that will handle the delegated tasks.
class OtherClassThatWantsToPerformTheActions {
// Creating the object that needs the delegate
// This can also be injected from a different object if we don't want to create it here.
let someObject = SomeObject()
init() {
// Assigning this class as the delegate object,
// now someObject can call the implemented methods in this class when needed.
someObject.delegate = self
}
}
// Implementing the delegate methods needed to be a valid delegate
extension OtherClassThatWantsToPerformTheActions: CustomDelegate {
func someDelegatedFunction() {
// Logic implemented when the method is called by the delegator
}
func someOtherDelegatedFunction(withString: String) {
// Logic implemented when the method is called by the delegator,
// In this case, the delegator is passing back some information we may need to perform the action.
}
}
Here are two crucial steps: we need to assign to SomeObject
the class that will handle the delegate, and we need to implement the delegate protocol in the delegate class so that the assigned object can respond to the actions of SomeObject
.
You’re done! Now, when you need to perform a delegated action, the responsible object will be called and perform the action in place of our delegator.
Example with UITableView
Let’s see an example involving UITableView
, which frequently uses the delegation pattern.
With objects managed by UIKit we only need to implement the step 3 of the previous example (for our custom objects we need to implement all steps ourselves).
We will make our ViewController
to act as the delegate for the UITableView
.
import UIKit
class ViewController: UIViewController {
// Creating the TableView that needs the delegate for certain actions
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
// Set up the table view
tableView.frame = view.bounds
tableView.delegate = self // assigning this object as the delegate
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
view.addSubview(tableView)
}
}
// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
// Return the number of rows in the section
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10 // Example number of rows
}
// Configure and return the cell for the given index path
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Row \(indexPath.row)"
return cell
}
}
// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {
// Handle row selection
// this method is defined in the delegate protocol for UITableView
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Selected row \(indexPath.row)")
tableView.deselectRow(at: indexPath, animated: true)
}
}
Both the delegate and the data source follow the delegation pattern, but the delegate manages user interactions and appearance, while the data source provides the data to be displayed.
In this example:
-
Define the Protocols: We use the built-in
UITableViewDelegate
andUITableViewDataSource
protocols. -
Create the Delegator: The
UITableView
instance is created and set up in theviewDidLoad
method. It is configured to use theViewController
as its delegate and data source. -
Implement the Delegate and Data Source: The
ViewController
class conforms to theUITableViewDelegate
andUITableViewDataSource
protocols. It implements required methods such astableView(_:numberOfRowsInSection:)
andtableView(_:cellForRowAt:)
for the data source, andtableView(_:didSelectRowAt:)
for the delegate.
Conclusion
The Delegation Pattern is a fundamental design pattern in iOS development that allows for a clear separation of concerns, making your code more modular and easier to manage. By delegating responsibilities, you can create reusable and flexible components that improve the maintainability of your codebase.
In this post, we explored the Delegation Pattern, learned how to implement it with a simple custom example, and saw how it is applied in a real-world scenario with UITableView.
Understanding and utilizing this pattern will help you create well-structured and efficient code, ensuring that your iOS applications remain robust and scalable. As always, following best practices and leveraging powerful design patterns like delegation will lead to better, more maintainable applications.
Happy coding!