Working with Delegation Pattern
1. Defining Delegate Protocols
| Protocol | Description | Usage |
|---|---|---|
| UIApplicationDelegate | Defines methods for managing the lifecycle of an iOS app | Used in SceneDelegate or AppDelegate |
| UITableViewDelegate | Handles user interactions with table view cells | Implement for custom cell behavior |
| UICollectionViewDelegate | Manages collection view interactions | Customize selection and layout behavior |
Example: Defining a Custom Delegate Protocol
protocol CustomDelegate: AnyObject {
func didTapButton()
func didChangeValue(to value: String)
}
class ViewController: UIViewController {
weak var delegate: CustomDelegate?
@IBAction func buttonTapped(_ sender: UIButton) {
delegate?.didTapButton()
}
}
2. Creating Weak Delegate Properties
| Property | Type | Description |
|---|---|---|
| delegate | SomeDelegateType? |
Weak reference to the delegate object |
Example: Setting up a weak delegate property
class MyViewController: UIViewController {
weak var delegate: MyDelegate?
}
protocol MyDelegate: AnyObject {
func didCompleteTask()
}
Note: Using
weak prevents retain cycles when the delegate holds a strong reference to the delegating object.3. Implementing Delegate Methods
| Delegate Method | Signature | Description | Notes |
|---|---|---|---|
| func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) |
Called when a row is selected in a table view | Use to handle selection logic or navigate to detail views |
| func textFieldDidChangeSelection(_ textField: UITextField) | func textFieldDidChangeSelection(_ textField: UITextField) |
Called when the text field's selection changes | Use to validate or update UI based on input |
| func scrollViewDidScroll(_ scrollView: UIScrollView) | func scrollViewDidScroll(_ scrollView: UIScrollView) |
Called when the scroll view is scrolled | Use to implement custom scroll behaviors or animations |
Example: Implementing a Table View Delegate Method
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedCell = tableView.cellForRow(at: indexPath)
print("Selected cell at row \(indexPath.row)")
}
}
Example: Implementing a Text Field Delegate Method
extension ViewController: UITextFieldDelegate {
func textFieldDidChangeSelection(_ textField: UITextField) {
guard let text = textField.text else { return }
if text.count > 10 {
print("Text is too long")
}
}
}
4. Using Optional Delegate Methods
| Method | Description | Notes |
|---|---|---|
| func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) | Called when a row is selected | Optional, not required for basic table functionality |
| func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) | Called when a row is deselected | Optional, only needed if custom deselection logic is required |
| func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) | Called just before a cell is displayed | Optional, useful for animations or lazy loading |
| func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) | Called after a cell is removed from display | Optional, useful for cleanup or memory management |
Example: Implementing Optional Delegate Method
class MyTableViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
}
}
extension MyTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// Animate the cell appearance
cell.alpha = 0
UIView.animate(withDuration: 0.3) {
cell.alpha = 1
}
}
}
Note: Optional delegate methods are not required to implement any specific behavior, but they provide hooks for customizing the UI or handling user interactions.
5. Understanding Delegate Pattern Benefits
| Benefit | Description | Example |
|---|---|---|
| Loose Coupling | Reduces direct dependencies between objects, allowing for more flexible and maintainable code. | |
| Code Reusability | Allows the same delegate to be used across multiple UI components or in different contexts. | |
| Separation of Concerns | Encourages separation of logic by delegating specific responsibilities to dedicated objects. | |
| Extensibility | Enables easy extension of functionality without modifying core classes. | |
Note: The delegation pattern is a fundamental design pattern in iOS development, particularly for handling user interactions and customizing UI behavior.
6. Using Multiple Delegates
When working with the delegation pattern in iOS Swift, it's common to encounter scenarios where a single object needs to conform to multiple delegate protocols. This is particularly useful in UIKit and AppKit frameworks where views often require multiple delegates for different behaviors.
| Protocol | Delegate Method | Description |
|---|---|---|
| UITableViewDelegate | didSelectRowAt |
Handles row selection in a table view |
| UIScrollViewDelegate | didScroll |
Responds to scroll events in a scroll view |
| UICollectionViewDelegate | didSelectItemAt |
Handles item selection in a collection view |
Example: Implementing Multiple Delegates
class MyViewController: UIViewController, UITableViewDelegate, UIScrollViewDelegate {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
scrollView.delegate = self
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Selected row at \(indexPath.row)")
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("Scrolled by \(scrollView.contentOffset.x)")
}
}
Note: When implementing multiple delegate protocols, ensure that the methods do not conflict or interfere with each other's functionality.
7. Implementing Common Delegates
| Delegate | Protocol | Usage | Notes |
|---|---|---|---|
| UITableViewDelegate | UITableViewDelegate |
Customize table view cell behavior, selection, and layout. | Used for customizing row height, editing, and selection behavior. |
| UICollectionViewDelegate | UICollectionViewDelegate |
Customize collection view cell behavior and selection. | Allows customization of layout, selection, and appearance. |
| UITextFieldDelegate | UITextFieldDelegate |
Control text field input and editing behavior. | Used for validating input, handling return key presses, and managing focus. |
| UITextViewDelegate | UITextViewDelegate |
Control text view input and editing behavior. | Handles text changes, selection, and interaction with the text view. |
| UIScrollViewDelegate | UIScrollViewDelegate |
Manage scroll view behavior and content offset changes. | Used to customize scrolling, zooming, and content layout. |
Example: Implementing UITableView Delegate
class MyTableViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
}
}
extension MyTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Selected row at \(indexPath.row)")
}
}
Example: Implementing UITextField Delegate
class MyViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
textField.delegate = self
}
}
extension MyViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textFieldDidChangeSelection(_ textField: UITextField) {
print("Text changed to: \(textField.text ?? "")")
}
}
Note: Delegates are commonly used in iOS development to customize the behavior of UI components. They provide a way to respond to user interactions and control how views behave.
8. Avoiding Retain Cycles with Delegates
When using delegation patterns in Swift, retain cycles can occur if both the delegate and the delegating object hold strong references to each other. This leads to memory leaks as neither object is deallocated.
| Problem | Solution | Notes |
|---|---|---|
| Strong reference cycle | weak or unowned self in closures |
Prevents circular reference between delegate and delegating object |
Example: Using weak to prevent retain cycle
class ViewController: UIViewController {
var delegate: MyDelegate?
override func viewDidLoad() {
super.viewDidLoad()
delegate = MyDelegate(viewController: self)
}
}
class MyDelegate {
weak var viewController: ViewController?
init(viewController: ViewController) {
self.viewController = viewController
}
func doSomething() {
// Safe to access viewController here without retain cycle
viewController?.view.backgroundColor = .red
}
}
Note: Use
weak for optional delegate properties and unowned when you're certain the delegate will not be deallocated before the object using it.Warning: Using
unowned incorrectly can lead to runtime crashes. Always prefer weak unless you're certain about the lifecycle.9. Using Closures vs Delegates
| Aspect | Closures | Delegates |
|---|---|---|
| Definition | Anonymous functions that can capture and store references to variables from their surrounding context. | Protocol-based pattern where one object forwards responsibility to another object. |
| Usage Context | Commonly used for callbacks, event handling, and asynchronous operations. | Used when you need to define behavior in a separate object or class, often for UI components like UITableViews. |
| Memory Management | Can lead to retain cycles if not handled carefully (e.g., using [weak self] in closures). | Less prone to retain cycles due to the protocol-based design; typically uses weak references in delegate properties. |
| Flexibility | Highly flexible and concise for simple tasks. | More structured and suitable for complex interactions or multiple responsibilities. |
| Code Readability | Can become less readable with complex logic or long closures. | Better for organizing code across multiple methods and classes, improving readability and maintainability. |
Example: Closure Usage
class Button {
var onTap: (() -> Void)?
func tap() {
onTap?()
}
}
let button = Button()
button.onTap = { print("Button tapped!") }
button.tap() // Output: Button tapped!
Example: Delegate Usage
protocol ButtonDelegate: AnyObject {
func buttonTapped(_ button: Button)
}
class Button {
weak var delegate: ButtonDelegate?
func tap() {
delegate?.buttonTapped(self)
}
}
class ViewController: ButtonDelegate {
func buttonTapped(_ button: Button) {
print("Button tapped!")
}
}
When to Use Which?
- Closures are ideal for short, self-contained actions or when you want to avoid creating a separate class or protocol.
- Delegates are better for more complex interactions or when you want to separate concerns and reuse logic across different parts of your app.
10. Implementing Protocol Witnesses
| Protocol | Method | Description |
|---|---|---|
| UITableViewDataSource | numberOfRowsInSection |
Returns the number of rows in a specified section. |
| UITableViewDataSource | cellForRowAt |
Provides the cell to display at a specified index path. |
| UITableViewDelegate | didSelectRowAt |
Handles row selection in a table view. |
| UICollectionViewDataSource | numberOfItemsInSection |
Returns the number of items in a specified section. |
| UICollectionViewDataSource | cellForItemAt |
Provides the cell to display at a specified index path. |
Example: Implementing UITableViewDataSource
class MyTableViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
}
}
extension MyTableViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = "Row \(indexPath.row)"
return cell
}
}
Note: Protocol witnesses must be implemented in an extension or within the class that conforms to the protocol.