Delegate: what it is, the delegation pattern and how it works on iOS

Author: IT Sectr Published: 2026-03-17 Reading time: 10 min

Delegate is a design pattern where one object delegates task execution to another object through a protocol with predefined methods. In iOS development, Delegate is one of the fundamental Cocoa Touch patterns, used for asynchronous notification without direct coupling between sender and receiver. According to Apple Documentation (2025), delegation is used in Foundation and UIKit for handling table events, network requests, and location management. The pattern ensures loose coupling of components and code reuse.

Key Takeaways

  • Delegate is a pattern where an object entrusts event handling to another object through a protocol.
  • Protocol in Swift defines a set of methods that a delegate must or may implement.
  • Weak reference is required for the delegate property to avoid retain cycles.
  • @objc optional allows making protocol methods optional for implementation.
  • URLSessionDelegate is an asynchronous delegate for handling network request events.

What is Delegate?

Delegate is an object that implements a specific protocol and receives notifications about events of another object. The Delegation pattern is an alternative to inheritance: instead of creating a subclass to override methods, an object delegates event handling to an external object. In iOS, delegation is implemented through Swift protocols with required and optional methods. The delegate property is always declared as weak var to avoid circular references between objects.

Defining the Delegate Protocol

A delegate protocol defines the interaction contract between objects. Required methods must be implemented by the delegate, otherwise the code will not compile. Optional methods are marked with the @objc optional attribute and allow the delegate to respond only to relevant events. Method names follow a convention: the first parameter is the sender object, the second is the event data. For example, tableView(_:didSelectRowAt:) indicates that the sender is UITableView and the data is the selected row index.

swift
// Delegate Protocol
protocol DownloadManagerDelegate: AnyObject {
    func downloadManager(_ manager: DownloadManager,
                          didFinishWith data: Data)
    func downloadManager(_ manager: DownloadManager,
                          didFailWith error: Error)

    @objc optional func downloadManager(_ manager: DownloadManager,
                                didUpdateProgress progress: Float)
}

// Class Using Delegate
class DownloadManager {
    weak var delegate: DownloadManagerDelegate?

    func startDownload(from url: URL) {
        URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
            guard let self else { return }
            if let error = error {
                self.delegate?.downloadManager(self, didFailWith: error)
            } else if let data = data {
                self.delegate?.downloadManager(self, didFinishWith: data)
            }
        }.resume()
    }
}

Weak Reference to Delegate

The delegate property must be declared as weak var to prevent retain cycles. If the reference were strong, the delegate and the delegating object would hold each other, and ARC would not be able to free their memory. Delegate protocols inherit AnyObject (classes only), which allows using weak. Structures and enums cannot be delegates due to value semantics. An alternative for value types is callback closures.

swift
class ViewController: DownloadManagerDelegate {
    let manager = DownloadManager()

    override func viewDidLoad() {
        super.viewDidLoad()
        manager.delegate = self // weak — no retain cycle
        manager.startDownload(from: url)
    }

    func downloadManager(_ manager: DownloadManager, didFinishWith data: Data) {
        processData(data)
    }

    func downloadManager(_ manager: DownloadManager, didFailWith error: Error) {
        showError(error)
    }
}

How Does the Delegate Pattern Work in iOS?

The Delegate pattern works on a one-to-one basis: one sender object can have only one delegate at a given time. When an event occurs, the sender checks if the delegate is set and calls the corresponding protocol method. The advantage over direct calls is that the sender does not know the delegate's type, only that it conforms to the protocol. This adheres to the Dependency Inversion Principle (DIP) from SOLID.

Delegate Lifecycle

The delegate is assigned through assignment: someObject.delegate = self. When the delegate is deallocated, the property automatically becomes nil due to weak semantics. Before calling a delegate method, the delegate is checked via optional chaining: delegate?.method(). If delegate is nil, the call is ignored without a crash. For optional protocol methods, an additional check is used: delegate?.responds(to: #selector(...)), although in Swift this check is usually implicit through optional method declaration.

Asynchronous Notification via Delegate

In a multithreaded environment, delegate is used for asynchronous result return. URLSession provides URLSessionDelegate with methods called when data is received, on timeout, or authentication error. Delegate methods run on URLSession's background queue, so dispatching to the main queue is required for UI updates. Asynchronous delegate does not block the calling thread, allowing other tasks to continue.

swift
class NetworkService: NSObject, URLSessionDataDelegate {
    private lazy var session = URLSession(
        configuration: .default,
        delegate: self,
        delegateQueue: OperationQueue()
    )
    private var receivedData = Data()

    func urlSession(_ session: URLSession,
                    dataTask: URLSessionDataTask,
                    didReceive data: Data) {
        receivedData.append(data)
        let progress = Float(receivedData.count) / Float(expectedSize)
        DispatchQueue.main.async {
            self.progressHandler?(progress)
        }
    }

    func urlSession(_ session: URLSession,
                    task: URLSessionTask,
                    didCompleteWithError error: Error?) {
        if let error = error {
            delegate?.networkService(self, didFailWith: error)
        } else {
            delegate?.networkService(self, didReceive: receivedData)
        }
    }
}

Delegate vs Callback: Approach Comparison

Delegate and Callback solve the same problem — asynchronous notification — but in different ways. Delegate uses a protocol with named methods, callback uses a closure with context capture. The choice depends on the number of events, signature complexity, and architectural preferences. Apple recommends delegate for APIs with multiple events (UITableView — 20+ methods) and callback for one-time completions.

When Delegate Wins

Delegate is preferable when handling multiple different events from a single source. For example, CLLocationManager notifies its delegate about location changes, permission errors, geo-fence entry/exit, and service status changes. Each event is a separate protocol method with a clear name and typed parameters. Delegate is also convenient for behavior configuration (should, will, did methods).

When Callback Wins

Callback is simpler for one-time requests with a single result. Completion handler in URLSession.dataTask takes one line at the call site versus at least three protocol methods. Callback is also more natural for functional chains (map, flatMap, async/await). However, with nesting beyond 2-3 levels, callback becomes Callback Hell, whereas delegate always remains flat.

Built-in Delegates in iOS SDK

The iOS SDK contains dozens of built-in delegate protocols for various subsystems. Each is designed for a specific interaction scenario. According to Apple Documentation (2025), the most used delegates are UITableViewDelegate, UITextFieldDelegate, CLLocationManagerDelegate, URLSessionDelegate, and UNUserNotificationCenterDelegate. These protocols contain 3 to 30 methods with varying levels of requirement.

UITableViewDelegate

UITableViewDelegate manages the appearance and behavior of table cells. It contains methods for handling row selection, configuring cell height, custom header/footer views, and swipe actions. All protocol methods are optional, allowing only the needed functionality to be implemented. Without a delegate, the table works with default settings. Historically, delegate was combined with UITableViewDataSource.

URLSessionDelegate

URLSessionDelegate provides detailed control over HTTP requests. Delegate methods are called when a server response is received, data arrives, or download completes. Specialized sub-protocols URLSessionTaskDelegate and URLSessionDataDelegate extend the base functionality for specific task types. Delegate is required for supporting background downloads, SSL certificates, and custom redirect handling.

DelegateMethodsPurpose
UITableViewDelegate25Table appearance and interaction
UITextFieldDelegate8Text input and keyboard handling
CLLocationManagerDelegate12Location updates and geofences
URLSessionDelegate6HTTP session and certificate management
UNUserNotificationCenterDelegate4Foreground push notification handling

Memory Management with Delegate

Memory management is a critical aspect of working with delegate in iOS. ARC (Automatic Reference Counting) automatically manages memory, but only with proper use of weak/unowned references. Violating the rules leads to memory leaks or premature deallocation. A delegate declared as strong creates a retain cycle if the delegate owner also holds a reference to the delegating object.

Retain Cycle via Delegate

A retain cycle occurs when object A (owner) sets itself as delegate of object B, and B holds a strong reference to the delegate. Example: ViewController creates URLSession, sets self as the session's delegate, but URLSession by default holds a strong reference to the delegate if delegateQueue is not specified. The solution is to always check the API documentation for the delegate reference type (weak or strong) and explicitly nil out the delegate in deinit.

swift
class SafeViewController: UIViewController {
    private var session: URLSession?
    private var service: NetworkService?

    override func viewDidLoad() {
        super.viewDidLoad()
        service = NetworkService()
        service?.delegate = self
    }

    deinit {
        // Set delegate to nil in deinit — best practice
        service?.delegate = nil
        session?.invalidateAndCancel()
    }
}

// URLSession with weak delegate via NSObject
class WeakDelegateSession: NSObject {
    private weak var delegate: URLSessionDelegate?

    func createSession() -> URLSession {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1
        return URLSession(
            configuration: .default,
            delegate: self,
            delegateQueue: queue
        )
    }
}

Safe Delegate Check Before Calling

Before calling a delegate method, you must verify that the delegate exists (not nil) and implements the called method. For required protocol methods, no check is needed — the compiler guarantees implementation. For optional methods, use respond(to:) or optional chaining. If the delegate is deallocated, the weak reference automatically becomes nil, and the delegate call is ignored. This is safe behavior that requires no additional handling.

Common Delegate Implementation Mistakes

Developers often make mistakes when working with the Delegate pattern, especially in the early stages of iOS learning. The most common ones include: retain cycle due to strong delegate, forgetting to call delegate?.method(), incorrect protocol method signatures, setting delegate after starting an operation, and multithreading collisions. Let's look at each mistake and how to prevent it.

Strong Reference Instead of Weak

The most critical mistake is declaring the delegate property as strong var instead of weak var. This creates a retain cycle where neither the delegate nor the delegating object can be freed. Consequences: memory leaks, app slowdown, and hidden bugs. Solution: always use weak var for delegate, and have the protocol inherit from AnyObject to prevent using value types as delegates.

Setting Delegate After Starting an Operation

If the delegate is set after calling an asynchronous method, the first events may be lost. Example: calling startDownload() before assigning manager.delegate = self results in missing the completion callback if the download executes synchronously or very quickly. Solution: set the delegate before calling the asynchronous method and document the initialization order in protocol comments.

Frequently Asked Questions

Why is delegate declared as weak?

Weak prevents a retain cycle between the delegate and the delegating object. If the reference were strong, objects would hold each other, and ARC would not be able to free them. A weak reference automatically becomes nil when the delegate is deallocated. This is a standard Cocoa Touch practice since the advent of Objective-C and is preserved in Swift for backward compatibility.

What is the difference between delegate and dataSource?

Delegate handles events and manages behavior (cell height, response to taps). DataSource provides data for display (number of rows, cells). The delegate answers "how?", dataSource answers "what?". In iOS, both are implemented through protocols, often in the same controller, but are conceptually separate.

Can a struct be used as a delegate?

No, if the protocol inherits from AnyObject (class protocol). Weak references are only available for reference types (classes). For value types (struct, enum), use callback closures or a separate wrapper class. If you control the protocol, you can avoid inheriting AnyObject, but then weak is prohibited — choose between weak delegate and struct delegate consciously.

What is the responds(to:) method and why is it needed?

responds(to:) is an NSObjectProtocol method that checks whether an object implements the specified selector. It is used to check optional @objc protocol methods before calling them. Without this check, calling an unimplemented optional method would lead to NSInvalidArgumentException. In Swift, for protocols with @objc optional, the check can be implicit through optional binding.

Is Delegate a singleton or not?

No, delegate is a delegation pattern, not a singleton. Unlike a singleton, a delegate can be replaced at runtime and exists in a single instance for each delegating object. One object can be a delegate for multiple senders. Singleton is a creational pattern that guarantees a single class instance, which has nothing to do with delegation.

Summary

  • Delegate is a pattern where an object delegates event handling to another object through a protocol with typed methods.
  • Weak var is required for the delegate property to prevent retain cycles and memory leaks.
  • Protocol defines required and optional (@objc optional) delegation methods.
  • iOS SDK contains 15+ built-in delegate protocols: UITableViewDelegate, URLSessionDelegate, CLLocationManagerDelegate.
  • Delegate is preferable to callback when there are 3+ different events from a single source (CLLocationManager).
  • Asynchronous delegate is used in URLSession for receiving data and progress without blocking the thread.
  • Set the delegate before starting an asynchronous operation and nil it out in deinit for safe memory management.

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also