Swift & Objective-C

Delegate vs. Closure: 3 Criteria for Choosing a Callback

Delegates and closures handle the same callback needs, but they have different trade-offs. This article implements the same example side by side, distills five practical differences, and summarizes how to choose based on event count, relationship lifetime, and whether a return value is needed.

6 min read
Cover image for Delegate vs. Closure: 3 Criteria for Choosing a Callback

“Should we extract this callback into a delegate or receive it as a closure?” It’s a question that comes up remarkably often in iOS code reviews.

Both tools handle the same requirement—“tell me when something happens”—so either can implement it from a purely functional perspective.

UIKit is full of delegates (UITableViewDelegate, UITextFieldDelegate). Yet Apple’s newer APIs accept closures, and team conventions vary.

This article implements both approaches side by side using the same example to expose their differences and establish selection criteria.

The syntax of the delegate pattern itself and its comparison with observers are covered in a separate article, while closure capture and retain-cycle mechanics are covered in the closure article. Here, we focus on the intersection of those two topics: choosing.

The Same Problem, Two Answers — Side-by-Side

Suppose we’re building an image picker screen. When the user selects a photo, we need to notify the screen that opened it.

Here is the delegate version.

protocol ImagePickerDelegate: AnyObject {
    func imagePicker(_ picker: ImagePickerVC, didSelect image: UIImage)
    func imagePickerDidCancel(_ picker: ImagePickerVC)
}

final class ImagePickerVC: UIViewController {
    weak var delegate: ImagePickerDelegate?

    private func selectionDone(_ image: UIImage) {
        delegate?.imagePicker(self, didSelect: image)
    }
}

// Caller
extension ProfileVC: ImagePickerDelegate {
    func imagePicker(_ picker: ImagePickerVC, didSelect image: UIImage) {
        avatarView.image = image
    }
    func imagePickerDidCancel(_ picker: ImagePickerVC) { /* Ignore */ }
}

Here is the closure version.

final class ImagePickerVC: UIViewController {
    var onSelect: ((UIImage) -> Void)?
    var onCancel: (() -> Void)?

    private func selectionDone(_ image: UIImage) {
        onSelect?(image)
    }
}

// Caller
let picker = ImagePickerVC()
picker.onSelect = { [weak self] image in
    self?.avatarView.image = image
}

The behavior is the same. The difference lies in the structure.

A delegate first declares the communication contract as a protocol type, and the receiver adopts the entire contract. A closure exchanges each event as a value without a contract.

This structural difference leads to the practical differences below.

Five Practical Differences

First, scalability as the number of events grows. With a delegate, even if events grow from 5 to 10, you add methods to the protocol.

Related events are grouped into one contract, and the adopter implements the entire “conversation with this screen” in one extension. That is why UITableViewDelegate remains manageable despite having dozens of methods.

With closures, one property is added for each event. Once you go beyond three or four, configuration becomes scattered, and the compiler cannot tell you which callback was never connected.

An unimplemented required delegate method causes a compile error, whereas a closure property can remain nil and fail silently.

Second, the distance between configuration points. A closure’s strength is that the code consuming an event sits right next to the call that triggers it.

The code opening the picker and receiving its result is within three lines, so the flow is immediately clear. With a delegate, configuration (delegate = self) and implementation (extension) are separated within the file.

That ceremony is excessive for one-off interactions. It is why closures have become standard for events that “happen once and finish,” such as network request completion, alert button responses, and animation completion.

Third, state and identity. Delegate methods conventionally receive the sender as their first argument (imagePicker(_:didSelect:)the picker).

That convention lets you distinguish which table view produced an event when one screen uses two table views.

A closure does not pass the sender automatically, so in the same situation you must install two closures or explicitly design the sender into the arguments.

Fourth, where memory-management traps appear. Both carry a risk of retain cycles, but the traps look different.

A delegate settles this at declaration time with weak var delegate one decision, and mistakes are uncommon because the convention is so strong. With closures, you must repeatedly decide whether to use [weak self] at every attachment point.

As covered in the ARC (Automatic Reference Counting) article, weak is unnecessary for a one-off execution with no ownership cycle, but a callback stored in a property is a potential cycle.

You make that judgment again at every use site. Closures expose more surface area for mistakes.

Fifth, testing and reuse. Closures are lightweight in tests.

You can attach the callback directly in the test body and verify whether it was called, without a mock object. Delegates require a test spy class, which adds setup code.

In return, a delegate protocol documents “how to communicate with this component.” When multiple screens reuse the same component, an explicit contract is a clear advantage.

A comparison illustration likening delegates and closures to a contract in a meeting room and a note at a service counter
A delegate is a meeting-room contract; a closure is a note at the service counter

Selection Criteria — Decide by Event Count, Lifetime, and Direction

Now that we know the differences, let’s condense them into criteria. Three questions cover most cases.

Question 1 — How many events are there? For one or two, use a closure; beyond three, or for a relationship expected to grow, use a delegate.

The closer an event group is to a “conversation,” the more valuable a contract (protocol) becomes.

Question 2 — What is the relationship’s lifetime? Use a closure when it ends quickly, like request-response; use a delegate for an ongoing relationship lasting as long as the screen, such as scroll events or validation during text editing.

For long-lived relationships, the safety of one weak delegate is more advantageous than deciding on [weak self] at every use site.

Question 3 — Do you need a value returned? Delegate methods can have return values.

textField(_:shouldChangeCharactersIn:) is the classic example. Query-style communication that asks “is this allowed?” is delegate territory.

You can design a return type for a closure too, but handling the return value of a stored optional closure—what is the default when it is nil?—becomes awkward.

There are certainly ambiguous cases. When that happens, Apple’s recent direction is a useful external signal.

UIAction-based button handlers and the closure provider of UICollectionViewDiffableDataSource are examples. The move to async/await—the generational replacement for completion handlers discussed in the error-handling article—is part of the same trend.

One-shot, data-providing communication continues to move toward closures and async.

By contrast, delegates remain the standard for continuous interaction. UITableViewDelegate and UINavigationControllerDelegate are examples.

This division of labor in frameworks aligns exactly with the three questions above.

One anti-pattern is worth calling out: creating a new delegate protocol with only one method for every screen.

Going through four layers of ceremony—protocol declaration, adoption, a weak property, and an extension—for a single event is usually overengineering. That is where one line of closure code belongs.

The opposite extreme—a class with six or seven closure properties—is a signal to consolidate them into a delegate.

A callback-selection flowchart branching on three questions: event count, relationship lifetime, and return value
Event count, relationship lifetime, and return value—three questions are enough to decide

Summary

  • Delegates and closures are two implementations of the same requirement (“tell me when something happens”). The fundamental difference is whether the contract is declared as a type (a protocol) or events are exchanged as values.
  • The five practical differences: delegates offer event scalability and detection of missing implementations; closures keep configuration cohesive; sender identification follows delegate conventions; closures expose more memory-management traps; and closures are lighter for testing.
  • Choose with three questions: are there at least three events, is the relationship long-lived, and is a return value needed? If none apply, use a closure; if even one strongly applies, use a delegate.
  • A single-method delegate protocol and five or six closure properties are overengineering signals pointing in opposite directions.

If you want the details of both tools, Continue reading. The delegate-pattern article compares it with observers and covers the canonical form of 1:1 communication.

The closure deep dive covers capture, weak self, and escaping.

Continue reading