Protocols

A protocol is a contract: a list of requirements with no implementation. Any type — struct, class, or enum — can promise to satisfy it, and code written against the protocol then works with every conforming type. This is how Swift shares behaviour without inheritance.

This lesson defines protocols, extends them with shared implementations, composes them, and finishes with the design rules that keep protocol-oriented code maintainable.

Defining Protocols

A protocol states what must exist and says nothing about how it is done. The declaration therefore contains no bodies: just the shape of the properties and methods that a conforming type must provide.

Method & Property Requirements

A property requirement declares readability, and optionally writability, with get or get set. A method requirement is an ordinary signature ending without braces. Marking a requirement mutating allows a struct to fulfil it.

// A protocol is a contract: it lists what must exist, never how it works.
protocol Drawable {
    // Read-only requirement: a stored property, a computed property, or even a
    // `let` constant can satisfy it — only the getter is needed.
    var area: Double { get }

    // Read-write requirement: the conforming type must allow assignment, so a
    // `let` constant can never satisfy this one.
    var name: String { get set }

    // A method requirement is a signature with no body.
    func draw() -> String

    // `mutating` lets a struct (or an enum) satisfy the requirement as well:
    // without the keyword, value types could not conform.
    mutating func scale(by factor: Double)
}

A requirement may also be static, in which case the conforming type supplies a type-level member. A protocol can demand an initializer too, by listing init(…) as a requirement.

Adopting a Protocol

Conformance is declared by naming the protocol after the type name and a colon. The compiler checks the complete list of requirements at that moment: forget one and the build fails with the missing member named, not with a mysterious crash later.

A protocol states requirements; a struct, a class, and an enum declare conformance to it

Figure 1 — the protocol states the requirements; each conforming type declares conformance and supplies the members, or inherits them from a protocol extension.

struct Circle: Drawable {
    var name = "circle"                    // satisfies `{ get set }` — it is a var
    var radius: Double

    // A computed property satisfies a `{ get }` requirement.
    var area: Double { 3.14159 * radius * radius }

    func draw() -> String { "circle of radius \(radius)" }

    // `mutating` here matches the requirement, so a struct may conform.
    mutating func scale(by factor: Double) {
        radius *= factor
    }
}

class Square: Drawable {
    var name: String
    var side: Double

    init(side: Double) {
        self.side = side
        self.name = "square"
    }

    var area: Double { side * side }
    func draw() -> String { "square of side \(side)" }

    // A class mutates the shared instance, so no `mutating` is needed.
    func scale(by factor: Double) { side *= factor }
}

An enum cannot satisfy a { get set } requirement, because enums hold no stored properties for the setter to write to. That is a good argument for requiring only { get } unless you truly need assignment.

// A getter-only contract is friendlier: it lets structs, classes, AND enums
// conform, since none of them has to offer a writable property.
protocol Labelled {
    var label: String { get }
}

enum Signal: Labelled {
    case red, amber, green

    var label: String {
        switch self {
        case .red:   "stop"
        case .amber: "wait"
        case .green: "go"
        }
    }
}

print(Signal.amber.label)   // wait — the enum satisfies the contract with a switch

Value or Reference Conformance

A protocol is silent about semantics. What gets copied when you assign a variable depends on the underlying type, not on the protocol it conforms to — conformance adds requirements, never behaviour you did not write.

// The struct keeps value semantics even while conforming to Drawable.
var a = Circle(name: "c", radius: 1)
var b = a                        // ordinary struct copy
b.scale(by: 2)
print(a.radius)                  // 1.0 — a is untouched

// The class keeps reference semantics, conformance or not.
let s = Square(side: 2)
let t = s                        // ordinary reference copy
t.scale(by: 3)
print(s.side)                    // 6.0 — both names see one object

// A protocol-typed variable boxes the value it holds. Calling a mutating
// requirement through it is legal when the variable itself is a `var`.
var shape: any Drawable = a
shape.scale(by: 3)               // mutates the boxed copy, not `a`
print(a.radius)                  // 1.0 — the original value is still 1.0

Protocol Extensions

A protocol declares requirements; an extension can implement them. That single feature removes most of the duplication that object-oriented code pays for, because one implementation serves every conforming type instead of one per class in a hierarchy.

Default Implementations

Any member added in a protocol extension becomes available to all conforming types. Implement it in the extension and it acts as a default that a type may still replace; add a brand-new method and it becomes a free capability nobody had to declare.

// The same protocol as before, stated with just two requirements for brevity.
protocol Drawable {
    var name: String { get }
    func draw() -> String
}

extension Drawable {
    // A brand-new method: not a requirement, granted to every conforming type.
    func describe() -> String {
        "\(name) -> \(draw())"
    }

    // A default body for a requirement. Types that say nothing inherit this one.
    func draw() -> String {
        "[\(name)]"
    }
}

struct Dot: Drawable {
    var name = "dot"                      // uses both extension members as-is
}

struct Star: Drawable {
    var name = "star"
    func draw() -> String { "*\(name)*" } // replaces the default implementation
}

print(Dot().describe())    // dot -> [dot]
print(Star().describe())   // star -> *star*

There is a sharp edge here, and it is worth memorising. A method that exists only in the extension is dispatched statically, so a type's own version is never reached through a protocol-typed variable.

protocol Speaker { }                       // no requirements at all

extension Speaker {
    func speak() -> String { "default voice" }
}

struct Robot: Speaker {
    // This does NOT override the extension method: the protocol never asked for
    // it, so the extension body is merely a method the struct also happens to have.
    func speak() -> String { "beep" }
}

let direct = Robot()
print(direct.speak())                      // beep — concrete type, its own method

let boxed: any Speaker = Robot()
print(boxed.speak())                       // default voice — extension, static dispatch

// The fix is to declare it as a requirement, so the implementation is looked up
// in the conforming type instead of being taken from the extension.
protocol Talker {
    func speak() -> String
}
extension Talker {
    func speak() -> String { "default voice" }
}

Constrained Extensions

An extension may restrict itself with a where clause, so the added capability exists only for the types where it is meaningful. This is how the standard library offers extra methods for numeric collections or comparable sequences.

// The capability appears only when the elements support + and a zero start.
extension Collection where Element: Numeric {
    func total() -> Element {
        reduce(0, +)
    }
}

print([1, 2, 3].total())         // 6
print([1.5, 2.5].total())        // 4.0
// print(["a", "b"].total())     // error: String is not Numeric

// A second constraint on a standard-library protocol.
extension Sequence where Element: Comparable {
    func largest() -> Element? {
        reduce(nil) { best, next in
            guard let best else { return next }      // first element becomes the seed
            return next > best ? next : best
        }
    }
}

print([3, 9, 4].largest() ?? 0)  // 9

// An extension can also add conformance for every type that already conforms.
extension Drawable: CustomStringConvertible {
    var description: String { describe() }
}

print(Dot())                     // dot -> [dot] — printed via description

Retroactive Conformance

Because conformance lives in an extension, you can make an existing type adopt your protocol without touching its declaration. The type does not even have to be yours — which is both the power and the danger of the technique.

// A type that already exists and that you may not own.
struct Temperature {
    var celsius: Double
}

protocol Formattable {
    func formatted() -> String
}

// Retroactive conformance: the extension is declared in YOUR module, while the
// type and the protocol come from somewhere else.
extension Temperature: Formattable {
    func formatted() -> String { "\(celsius)°C" }
}

print(Temperature(celsius: 21).formatted())   // 21.0°C

// Both the type and the protocol are non-local here, so keep this conformance
// in exactly ONE module. If two modules declare it, the two conformances can
// disagree and the compiler may reject the ambiguity at the point of use.

Composition & Abstraction

Swift offers three ways to talk about "some type that conforms": listing several protocols with &, storing a value as any protocol, and hiding the concrete type behind some. Knowing which one to use is the difference between clear code and code that boxes everything for no reason.

The & Composition Operator

Composition demands that a value satisfies several contracts at once. It replaces the "one protocol per role, glued by a base class" pattern with a list joined by &.

protocol Named { var name: String { get } }
protocol Aged  { var age: Int { get } }

// The argument must satisfy BOTH contracts — not one or the other.
func introduce(_ person: any Named & Aged) -> String {
    "\(person.name), age \(person.age)"
}

struct Cat: Named, Aged {
    var name: String
    var age: Int
}

print(introduce(Cat(name: "Ada", age: 3)))   // Ada, age 3

// Name a combination once and reuse it everywhere.
typealias Pet = Named & Aged

func feed(_ pet: any Pet) {
    print("feeding \(pet.name)")
}
feed(Cat(name: "Ada", age: 3))               // feeding Ada

Existentials with any

any Drawable is an existential: a box that holds a value whose concrete type is decided at run time. It buys you heterogeneous collections — many different types in one array — at the cost of a box and a table lookup per call.

// One array, two different concrete types: only possible through `any`.
var shapes: [any Drawable] = [
    Circle(name: "c", radius: 1),
    Square(side: 2)
]

for shape in shapes {
    // Requirements and extension members are reachable through the box.
    print(shape.describe())
}

// Members that belong only to a concrete type are not: the contract does not
// mention `radius`, so the compiler cannot promise it exists.
// print(shapes[0].radius)         // error: value of type `any Drawable` has no member

// Downcast when you genuinely need the concrete type back.
if let circle = shapes[0] as? Circle {
    print(circle.radius)           // 1.0
}

// The conforming type can also be changed after the box exists.
shapes[0] = Circle(name: "big", radius: 10)
print(shapes[0].describe())

Opaque Types with some

some Drawable is an opaque type: one specific type, known to the compiler, hidden from the caller. Because the compiler still knows it, calls stay direct and no box is allocated — while the function remains free to change its implementation later without breaking anyone.

// The concrete type is fixed, but the caller is not told which one it is.
func makeShape() -> some Drawable {
    Circle(name: "c", radius: 1)
}

let hidden = makeShape()
print(hidden.area)          // 3.14159 — members of the concrete type stay reachable
print(hidden.describe())    // c -> circle of radius 1.0

// Every return path must produce the SAME concrete type, because `some` names
// exactly one type. This is rejected at compile time:
// func broken(_ flag: Bool) -> some Drawable {
//     flag ? Circle(name: "c", radius: 1) : Square(side: 2)   // error
// }

// SwiftUI uses this everywhere: `some View` hides a large nested type while
// keeping the result one fixed concrete type for the compiler.
Aspectany Protocolsome Protocol
Concrete type Chosen at run time; may differ per value. Fixed at compile time; one type per declaration.
Dispatch Witness table, with a box for the value. Static — a direct call, no box.
Mixed collections Yes: [any Drawable] holds any conforming type. No: every element would have to be the same type.
Typical use Storage, arrays of mixed types, APIs across modules. Factory functions, computed properties, view builders.

Protocol-Oriented Design

Swift's standard library is built from protocols: Equatable, Hashable, Comparable, Sequence, Collection, Codable. Designing your own types the same way means composition instead of a deep hierarchy — and it works for value types, which inheritance never can.

Protocol or Superclass?

Both mechanisms share behaviour. The table makes the trade-off explicit so you can choose with reasons rather than habit.

Delegation

Delegation is the pattern protocols were made for in application code: an object performs a task and reports back through a protocol-typed property, so it never needs to know which class is listening. The classic example is a loader telling a screen that it finished.

// The delegate contract. `AnyObject` restricts conformance to classes, which is
// what makes a `weak` reference to the delegate possible in the first place.
protocol LoaderDelegate: AnyObject {
    func loaderDidFinish(_ count: Int)
}

extension LoaderDelegate {
    // A default body makes the requirement optional for delegates that do not
    // care about this particular event.
    func loaderDidFinish(_ count: Int) { }
}

final class Loader {
    // `weak` breaks the cycle: the loader must not keep its delegate alive.
    weak var delegate: (any LoaderDelegate)?

    func run() {
        let count = 3
        // Optional chaining: if nobody subscribed, nothing happens and nothing crashes.
        delegate?.loaderDidFinish(count)
    }
}

final class Screen: LoaderDelegate {
    var message = "idle"

    func loaderDidFinish(_ count: Int) {
        message = "loaded \(count) items"
    }
}

let screen = Screen()
let loader = Loader()
loader.delegate = screen          // Screen subscribes to the loader's events
loader.run()
print(screen.message)             // loaded 3 items

Common Pitfalls

Five rules cover almost every mistake made with protocols in production code.

// 1. A method that exists only in an extension is statically dispatched.
//    Declare it as a requirement whenever conforming types must customise it.

// 2. `any Protocol` boxes the value, so `[any Drawable]` allocates on every
//    element. Prefer a generic parameter or `some` when one concrete type is enough.

// 3. A `{ get set }` requirement cannot be met by an enum or by a `let`
//    constant. Ask for `{ get }` unless assignment is genuinely required.

// 4. Keep each protocol a single ROLE. Composing two small protocols with `&`
//    is clearer than one protocol that lists unrelated requirements.

// 5. A delegate property must be `weak` and the protocol `AnyObject`-bound,
//    or the two objects keep each other alive forever (see Memory Management).

Practice: define a Storage protocol with save and load, give load a default implementation in an extension, then write two conforming types — a struct backed by a dictionary and an enum that models a fixed table. Finally, write one function that accepts any Storage and see how little it needs to know.

Protocols describe capabilities; generics make them reusable without boxing. The next lesson, Generics, takes the same requirements and turns them into type parameters with constraints.


AspectProtocol + extensionSuperclass inheritance
Who can adopt it Struct, class, and enum. Classes only.
How many Unlimited; combine roles with &. Exactly one superclass.
Shared code Written once in a protocol extension. Inherited down the chain, whether needed or not.
Semantics Value or reference, chosen by the conforming type. Always a reference: instances live on the heap.
Effect of a change Add a default and existing types keep compiling. Changing a base method ripples into every subclass.
Reach for it when Capabilities combine and vary by role.