Table of Contents

Ⅰ.Introduction

One of the most powerful and elegant patterns in iOS development is the protocol-delegate mechanism. It enables flexible communication between objects without creating tight coupling — a core principle in clean architecture.

Ⅱ.What is a Protocol?

In Swift, a protocol is like an interface — it defines a set of methods or properties that a conforming type must implement.

A Swift Protocol is Swift’s version of an Interface in object-oriented programming (OOP). If you’re new to OOP, think of an interface as a blueprint that any class, struct, or enum adopting it must follow.

For example, imagine a Vehicle interface that declares a function startEngine(). In Swift, any type conforming to the Vehicle protocol must implement startEngine(). This is known as Protocol Conformance.

1
2
3
4
5
6
7
8
9
10
// Swift code example
protocol Vehicle {
func startEngine()
}

struct Car: Vehicle {
func startEngine() {
// Implementation for car
}
}

Ⅲ.Why Use Protocols?

Swift protocols establish contracts between different components without exposing unnecessary details. For instance, a vehicle’s user doesn’t need to know how an ATV or spaceship starts its engine—just that they can call startEngine().

This is where protocols come in: they let us define a series of properties and methods that we want to use. They don’t implement those properties and methods – they don’t actually put any code behind it – they just say that the properties and methods must exist, a bit like a blueprint.

For example

Think about how we might write some code to simulate someone commuting from their home to their office. We might create a small Car,train ,busstruct,or they might use a bike, or any number of other transport options.

Now, each transport option behaves differently:

  • A train is fast but limited to tracks.
  • A bike is slow but flexible.
  • A car is decent, but might get stuck in traffic.

In real-world development, without protocols, we would likely need to write different commute functions for each type of transportation, each requiring different parameters and implementations. For example, each type of transportation might have its own way of calculating time and how to perform the actual act of moving.
As a result, the code would become cumbersome and hard to maintain. You might end up writing multiple functions like this:

1
2
3
func commuteByCar(distance: Int) { ... }
func commuteByTrain(distance: Int) { ... }
func commuteByBike(distance: Int) { ... }

Now imagine if you later want to add a plane, subway, or electric scooter — your code will get messy quickly.Consider the example , without protocols, we might have to write separate functions for each transportation mode

Instead of writing separate functions for each transport type, you can define a protocol to describe what all vehicles have in common:

1
2
3
4
protocol Vehicle {
func estimateTime(for distance: Int) -> Int
func travel(distance: Int)
}

This protocol acts like a contract. It doesn’t care how a vehicle works — it just says, “You must be able to estimate time and travel a given distance.”

Now we can make a Car conform to this protocol:

1
2
3
4
5
6
7
8
9
struct Car: Vehicle {
func estimateTime(for distance: Int) -> Int {
return distance / 40 // 40 km/h
}

func travel(distance: Int) {
print("Driving a car for \(distance) km.")
}
}

We can do the same for a Bike:

1
2
3
4
5
6
7
8
9
struct Bike: Vehicle {
func estimateTime(for distance: Int) -> Int {
return distance / 15
}

func travel(distance: Int) {
print("Riding a bike for \(distance) km.")
}
}

Now Here’s the Magic:

We can now write one universal function that works with any vehicle that conforms to Vehicle:

1
2
3
4
5
func commute(distance: Int, using vehicle: Vehicle) {
let time = vehicle.estimateTime(for: distance)
print("Estimated time: \(time) hours")
vehicle.travel(distance: distance)
}

Usage:

1
2
3
4
5
6
7
8
9
10
11
12
let car = Car()
let bike = Bike()

commute(distance: 60, using: car)
// Output:
// Estimated time: 1 hours
// Driving a car for 60 km.

commute(distance: 60, using: bike)
// Output:
// Estimated time: 4 hours
// Riding a bike for 60 km.
  1. The protocol Vehicle is like a blueprint or pass — as long as a type follows the rules (implements the required methods), it can be passed into our commute function.
  2. We don’t care which vehicle is being used. We only care that it knows how to estimate time and move.
  3. This makes our code cleaner, easier to read, and more scalable.
  4. If we want to add a Plane or Subway, we don’t need to change commute() at all — just make them conform to Vehicle.

Ⅳ.What is a Delegate?:

Think of a delegate as someone you appoint to make a decision or perform a task on your behalf.

Imagine you’re not commuting alone. You have a personal assistant — someone you trust to choose the best vehicle for your daily commute.

You don’t choose the car, bus, or bike yourself.
You say to your assistant:
“Here’s the distance. You decide which vehicle to use, and let me know how long it will take.”

That assistant is your delegate.

In Programming Terms:

A delegate is a reference to another object that responds to certain events or provides custom behavior.

You (the main object) define what needs to happen,
but you let someone else (the delegate) decide how exactly it should happen.

This pattern:

  • Keeps your code decoupled
  • Allows customization
  • Follows the protocol contract for communication

Ⅳ.Why Use Delegate?

Let’s Build a Transportation Example with Delegate

Step 1: Define a Delegate Protocol:
1
2
3
protocol CommuteDelegate {
func didChooseVehicle(_ vehicle: Vehicle)
}

This says:

“If you’re going to be my assistant (delegate), you must tell me which vehicle you chose.”

Step 2: Define a Commuter Class That Uses the Delegate:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Commuter {
var delegate: CommuteDelegate?

func startCommute(distance: Int) {
print("Commuter: Asking delegate to choose vehicle...")

// Let delegate decide
if let delegate = delegate {
let vehicle = delegate.didChooseVehicle()
commute(distance: distance, using: vehicle)
} else {
print("No delegate set! Cannot commute.")
}
}

private func commute(distance: Int, using vehicle: Vehicle) {
let time = vehicle.estimateTime(for: distance)
print("Estimated time: \(time) hours")
vehicle.travel(distance: distance)
}
}
Step 3: Create a Delegate That Implements the Decision Logic:
1
2
3
4
5
6
7
class Assistant: CommuteDelegate {
func didChooseVehicle() -> Vehicle {
// For example: if it's raining, choose a car; else use a bike
print("Assistant: I choose a car today.")
return Car()
}
}
Step 4: Use It All Together:
1
2
3
4
5
let commuter = Commuter()
let assistant = Assistant()

commuter.delegate = assistant
commuter.startCommute(distance: 50)
Output:
1
2
3
4
Commuter: Asking delegate to choose vehicle...
Assistant: I choose a car today.
Estimated time: 1 hours
Driving a car for 50 km.

Key Takeaways

Concept Analogy Swift Term
delegate Your assistant Another object you trust to do part of the job
protocol Rules for being your assistant Contract they must follow
delegate property The assistant you hired delegate variable
delegate method “Choose the vehicle” didChooseVehicle()

A delegate is an object you assign to perform tasks or respond to events on your behalf.

It must conform to a protocol, which defines what methods it must implement.

This allows custom logic to be injected without hard-coding it into the main object — keeping code flexible, reusable, and clean.

Ⅴ.How to Use the Protocol-Delegate Pattern in Swift

The Protocol-Delegate pattern is a fundamental communication technique in Swift. It allows one object to delegate responsibility to another, promoting loose coupling, modularity, and code reuse.

Here’s a general step-by-step guide to using it in any Swift project:

1. Define a Protocol

1
2
3
protocol MyDelegate: AnyObject {
func didCompleteTask()
}

AnyObject is used to restrict the protocol to class types, which is important for weak references.

2. Add a Delegate Property

In the “sender” (the class that needs to report back), declare a delegate property.

1
2
3
4
5
6
7
8
class TaskPerformer {
weak var delegate: MyDelegate?

func performTask() {
// Do something...
delegate?.didCompleteTask()
}
}

weak is essential to avoid retain cycles (especially when used in UIViewControllers).

3. Conform to the Protocol

In the “receiver” (usually a controller), conform to the protocol and implement the required methods.

1
2
3
4
5
class ViewController: UIViewController, MyDelegate {
func didCompleteTask() {
print("Task completed! 🎉")
}
}

4. Set the Delegate

1
2
3
let performer = TaskPerformer()
performer.delegate = self
performer.performTask()

This pattern acts like a bridge between performTask() or didCompleteTask(), allowing them to communicate indirectly.
The delegate serves as a messenger, informing each side when to respond or transition.

Ⅵ.Protocol-Delegate Pattern in UIkit with Real Examples

Example 1: RingViewDelegate – Custom View Delegation

I created a custom view called RingView that animates a workout progress ring. However, RingView should not be responsible for what happens after the animation ends.

So I declared a protocol:

1
2
3
protocol RingViewDelegate: AnyObject {
func animationDidEnd()
}

In the controller:

1
2
3
4
5
6
7
8
9
10
11
class WorkoutViewController: UIViewController {
override func viewDidLoad() {
ringView.delegate = self
}
}

extension WorkoutViewController: RingViewDelegate {
func animationDidEnd() {
// Decide whether to move to next exercise or show results
}
}

This separates UI logic from business logic, making both components reusable.

Example 2: WorkoutDelegate – View Controller Communication

When the workout ends, I present a results page:

1
performSegue(withIdentifier: "WorkoutResultsSegue", sender: nil)

But how does the WorkoutResultsViewController tell the previous controller “Hey, I’m done”?

Again, with a delegate:

1
2
3
protocol WorkoutDelegate: AnyObject {
func dismissWorkout()
}
1
2
3
4
5
class WorkoutViewController: UIViewController, WorkoutDelegate {
func dismissWorkout() {
dismiss(animated: true)
}
}

Then in WorkoutResultsViewController, I use it like this:

1
2
3
4
5
@IBAction func submitButtonTapped(_ sender: Any) {
dismiss(animated: true) {
self.workoutDelegate?.dismissWorkout()
}
}

This is a perfect example of loosely coupled controller-to-controller communication.

In real-world iOS development, you should never assume a strict dependency between two view controllers — that one must always appear before the other, or that they must know each other’s internal structure.

Instead, using a delegate is like assigning a messenger between the two. The delegate handles communication without requiring a tight bond between the sender and receiver.

The protocol-delegate pattern isn’t just a nice-to-have in Swift — it’s a fundamental building block for writing maintainable, modular apps.

Ⅶ.Why Use the Protocol-Delegate Pattern in Swift?

1. Decoupling Logic for Flexibility

When you use a delegate, the object sending the message doesn’t need to know who receives it or how it will respond. This promotes loose coupling — a design principle that keeps your components flexible and easier to change.

Your views don’t need to know your controllers. Your controllers don’t need to hardcode downstream effects.

2. Promotes Code Reuse

By defining behaviors in protocols, you can swap out the delegate object at any time — as long as it conforms. This makes your views, components, and logic reusable across different parts of your app.

3. Encourages Clean Architecture

Separating what needs to happen (protocol) from how it happens (delegate implementation) allows you to follow the Single Responsibility Principle.

This separation keeps your views lightweight, your business logic testable, and your app architecture scalable.

4.Real-world Use Cases

This pattern is used all over the UIKit framework:

  • UITableViewDelegate
  • UICollectionViewDelegate
  • CLLocationManagerDelegate
  • URLSessionDelegate

Even Apple builds their frameworks around this principle — and for good reason.

Ⅷ.Summary

Protocols define the “what,” delegates provide the “how.”

The protocol-delegate pattern helps you build efficient, testable, and modular iOS apps by enabling components to talk to each other without tightly binding them together.

Whether you’re managing custom views, communicating between view controllers, or building scalable architecture — mastering this pattern will make you a better Swift developer.

Ⅸ.References


  1. Apple Developer Documentation. Protocols
  2. Swift Delegate: An In-Depth Guide for Structured iOS Programming. Swift Delegate: An In-Depth Guide for Structured iOS Programming
  3. A Step-by-Step Guide to the Delegate Pattern in Swift. https://medium.com/@afonso.script.sol/a-step-by-step-guide-to-the-delegate-pattern-in-swift-91a28de1baf8
  4. SwiftにおけるDelegateとは何か、なぜ使うのかSwiftにおけるDelegateとは何か、なぜ使うのか