Skip to main content

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.
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
    }
}
Code Reusability Allows the same delegate to be used across multiple UI components or in different contexts.
class CustomDelegate: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // Handle selection
    }
}
Separation of Concerns Encourages separation of logic by delegating specific responsibilities to dedicated objects.
class DataHandler: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data.count
    }
}
Extensibility Enables easy extension of functionality without modifying core classes.
class ExtendedDelegate: NSObject, UITableViewDelegate {
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        // Custom behavior
    }
}
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.

Was this article helpful?