Skip to content

4. Protocols & extensions

What & why

A protocol describes what a type can do — a set of properties and methods — without saying how. Any struct, class, or enum can conform. Combined with extensions (adding behaviour to existing types, even ones you don't own), protocols are the backbone of Swift's design: Apple famously calls it a protocol-oriented language.

Defining and conforming

protocol Shape {
    var name: String { get }          // readable property
    func area() -> Double
}

struct Circle: Shape {
    let radius: Double
    var name: String { "circle" }
    func area() -> Double { Double.pi * radius * radius }
}

struct Square: Shape {
    let side: Double
    let name = "square"
    func area() -> Double { side * side }
}

let shapes: [any Shape] = [Circle(radius: 1), Square(side: 2)]
for s in shapes {
    print(s.name, s.area())
}

any Shape means "some value of any type conforming to Shape" — an existential. Because the array holds different concrete types, you need any. The Advanced level digs into any vs some.

Requirements you can state

protocol Resettable {
    mutating func reset()             // mutating: structs may change themselves
}

protocol Named {
    var name: String { get set }      // must be readable AND writable
    init(name: String)                // initializer requirement
    static var typeLabel: String { get }
}

Extensions

Add methods, computed properties, initializers, subscripts, nested types, and conformances to an existing type:

extension Int {
    var isEven: Bool { self % 2 == 0 }
    func squared() -> Int { self * self }
}
print(4.isEven, 5.squared())        // true 25

extension String {
    var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) }   // needs Foundation
}

Extensions can't add stored properties — only computed ones.

Use extensions to organise your own types, too — one extension per protocol conformance reads well:

struct User {
    var name: String
    var age: Int
}

extension User: CustomStringConvertible {
    var description: String { "\(name) (\(age))" }
}
print(User(name: "Ada", age: 36))    // Ada (36)

Default implementations: the superpower

Extend a protocol to give every conforming type behaviour for free:

protocol Greeter {
    var name: String { get }
}

extension Greeter {
    func greet() -> String { "Hello, \(name)!" }      // available to every Greeter
}

struct Person: Greeter { let name: String }
print(Person(name: "Ada").greet())    // Hello, Ada!

Conforming types may supply their own version, replacing the default. (A subtlety: only methods that are declared in the protocol are dynamically dispatched; extension-only methods are resolved statically based on the variable's declared type. Declare a method in the protocol if you want overriding to work through a protocol-typed variable.)

Constrained extensions

Offer behaviour only when conditions hold:

extension Collection where Element: Numeric {
    func total() -> Element { reduce(0, +) }
}
print([1, 2, 3].total())          // 6
print([1.5, 2.5].total())         // 4.0

extension Array where Element == String {
    func bulleted() -> String { map { "• \($0)" }.joined(separator: "\n") }
}

Protocol composition and inheritance

protocol HasID { var id: Int { get } }
protocol Describable { var summary: String { get } }

func show(_ item: HasID & Describable) {      // composition: must be both
    print(item.id, item.summary)
}

protocol Entity: HasID, Describable {}        // inheritance: Entity implies both

Standard-library protocols you should know

Protocol Gives you
Equatable ==
Hashable use in Set / as Dictionary key
Comparable <, >, sorted(), min()/max()
CustomStringConvertible what print shows
Codable (Encodable & Decodable) JSON / plist encoding
Identifiable id property (used by SwiftUI lists)
Sequence / Collection for-in, map, filter… for free
Error usable with throw
Sendable safe to share between concurrency domains

Making your own sequence

struct Countdown: Sequence, IteratorProtocol {
    var current: Int
    mutating func next() -> Int? {
        guard current > 0 else { return nil }
        defer { current -= 1 }
        return current
    }
}
print(Array(Countdown(current: 3)))     // [3, 2, 1]
print(Countdown(current: 5).filter { $0 % 2 == 1 })   // [5, 3, 1]

Implement one method and you get the entire Sequence algorithm toolbox.

Protocols vs. class inheritance

  • A type can conform to many protocols but inherit from only one class.
  • Value types (structs/enums) can conform — they can't inherit.
  • Protocols describe capability ("can be drawn"), inheritance describes identity ("is a kind of view").
  • Prefer protocols + composition by default; use class inheritance when you truly have an "is-a" hierarchy or must subclass a framework class.

Class-only protocols

protocol Delegate: AnyObject {      // only classes can conform
    func didFinish()
}

AnyObject enables weak references to the conforming object — necessary for delegate patterns that avoid retain cycles (see the ARC lesson).

Protocols as test seams

Depend on a protocol and you can swap in a fake:

protocol Clock { func now() -> Date }

struct SystemClock: Clock { func now() -> Date { Date() } }
struct FixedClock: Clock {
    let date: Date
    func now() -> Date { date }
}

func greeting(clock: any Clock) -> String {
    Calendar.current.component(.hour, from: clock.now()) < 12 ? "Good morning" : "Hello"
}

Tests pass a FixedClock — no waiting for noon.

Common mistakes

  • Reaching for a class hierarchy when a protocol + default implementation would be simpler.
  • Expecting overriding to work for extension-only methods (see the dispatch note above).
  • Forgetting any in [Shape] — modern Swift requires [any Shape].
  • Trying to add stored properties in an extension.
  • Huge protocols. Many small protocols (Readable, Writable) compose better than one giant one.
  • Protocols with Self or associated-type requirements used as plain types — those need generics or any (next lessons).

Exercises

  1. Define protocol Animal { var name: String { get }; func speak() -> String } and three conforming types; store them in [any Animal].
  2. Add a default describe() to Animal via an extension, and override it in one type.
  3. Extend Array where Element: Comparable with isSorted() -> Bool.
  4. Make a struct Fibonacci: Sequence with its own iterator, then print the first 10 values with .prefix(10).
  5. Conform a Temperature struct to Comparable and CustomStringConvertible; sort an array of them.
  6. Define a Storage protocol (save(_:), load()), write InMemoryStorage and FileStorage, and write a function that works with either.

Recap

  • Protocols declare requirements; structs, enums, and classes can all conform.
  • Extensions add behaviour to any type and provide default protocol implementations.
  • Constrained extensions (where Element: Numeric) target specific cases.
  • Conform to the standard protocols (Equatable, Hashable, Comparable, Codable, Sequence) to unlock the standard library.
  • Prefer composition via protocols over deep class hierarchies; use protocols to make code testable.

Next: Generics →