Delegation In Detail
Delegate pattern
Delegation is defined as passing responsibility for the behavior implementation from one instance to another.
When an instance delegates some of its behavior, it speaks: “I don’t know how to implement this method. Please do it for me”.
In this pattern, the delegating object
(the delegate) has a reference to another object (the delegate object) and calls methods on it to inform it about events or to ask for information.
Simple Example
Suppose we have a Person
class that represents a person with a name. We want to delegate the responsibility of greeting the person to another object.
First, let’s define a protocol called GreetingDelegate
:
protocol GreetingDelegate {
func greet(name: String)
}
Next, let’s create the Person
class:
// Person is Delegating object
class Person {
let name: String
var delegate: GreetingDelegate?
init(name: String) {
self.name = name
}
//delegating object callls delegate
func sayHello() {
delegate?.greet(name: name)
}
}
In the Person
class, we declare a name
property to hold the name of the person and a delegate
property of type GreetingDelegate?
. When the sayHello()
method is called, the person notifies the delegate by calling the greet(name:)
method.
Now, let’s create a class that acts as the delegate:
// delegate object
class Greeter: GreetingDelegate {
func greet(name: String) {
print("Hello, \(name)!")
}
}
- We create instances of
Person
andGreeter
classes. - We assign the
Greeter
instance as the delegate of thePerson
instance by setting thedelegate
property. - When the
sayHello()
method is called on thePerson
, it notifies theGreeter
delegate by calling thegreet(name:)
method, and the greeter prints "Hello, Abdul!" to the console.
let person = Person(name: "Abdul")
let greeter = Greeter()
// Assign the greeter as the delegate of the person
person.delegate = greeter
// When the person wants to say hello
person.sayHello()
Why do we need a delegate?
Delegation is a great tool for object composition, which is an alternative to inheritance. it helps decouple class from concrete behavior thus allowing polymorphism so clients can implement their own behavior into the framework. UITableview is a prime example from UIKit which provides abstractions in the form of UITableviewDelegate and UITableViewDataSource and we as clients provide the implementation so it’s up to us to provide the behaviour.
let us take the example above we injected greeting behaviour into the person class.
however, we can also have other behaviors using polymorphism for the person class.
so to prevent person class from being coupled with the specific implementations we use the delegate to make it possible to have many implementations.
UIKit Example
Similarly preventing table view from being coupled with specific implementation table view delegates and data sources makes it possible to have many implementations to get different behaviours which depends on your implementations of these delegate and data source methods.
UITableView and its abstractions live in their own module which we call UIKit which is hidden from us by Apple. However, the implementations of these abstractions live in our module or app. Let’s take a simple example.
MyTableViewController conforms to the UITableViewDataSource and UITableViewDelegate and provides its own implementations of datasource and delegate methods
class MyTableViewController: UIViewController {
// UITableView instance
let tableView = UITableView()
// Data source
let items = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]
override func viewDidLoad() {
super.viewDidLoad()
// Setup table view
tableView.frame = view.bounds
tableView.dataSource = self
tableView.delegate = self
view.addSubview(tableView)
}
}
// MARK: - UITableViewDataSource
extension MyTableViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") ?? UITableViewCell(style: .default, reuseIdentifier: "Cell")
cell.textLabel?.text = items[indexPath.row]
return cell
}
}
// MARK: - UITableViewDelegate
extension MyTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Selected item: \(items[indexPath.row])")
}
}
MyTableviewcontroller provides it’s own implementation while someother class may provide it’s own implementation so apple made tableview in a way that we as clients can provide our own behaviour using our own implementation . imagine if tableview delegate and datasource were tied to a concrete type.
Benefits
Delegation is a powerful design pattern in iOS development that offers several benefits:
- Decoupling and Separation of Concerns: Delegation helps in separating responsibilities between objects. By delegating tasks to another object, you create a clean separation of concerns, where each object has a single responsibility.
- Reuse and Composition: Delegation promotes code reuse by allowing the same class to be reused in different contexts with different behaviors provided by different delegates. It encourages composition over inheritance, as behavior can be composed dynamically at runtime.
- Loose Coupling: Delegation promotes loose coupling between objects. The delegate object does not need to know the internal details of the delegating object, reducing dependencies and making the codebase more modular and easier to maintain.
- Testability: Delegation simplifies unit testing by enabling the use of mock objects for testing. Mocking the delegate object allows for isolated testing of the delegating object’s behavior, making it easier to write unit tests with predictable results.
- Event Handling: Delegation is commonly used for event handling in iOS development. UI controls like buttons, table views, and collection views use delegation to notify their delegates of user interactions or state changes, enabling custom responses to these events.