Lesson 13 / 25

Protocols and Extensions

Define protocols, conform types to them and add behaviour with extensions.

Contracts and retroactive behaviour

A protocol defines requirements (methods, properties, initialisers) that conforming types must provide. Structs, enums and classes can all conform to many protocols, which is how Swift shares behaviour without inheritance. Extensions add methods, computed properties, initialisers and protocol conformances to existing types, even ones you did not write, such as adding a formattedINR property to Double. Protocol extensions provide default implementations for requirements, so conforming types get behaviour for free and override only what they need. Swift can synthesise common conformances automatically: Equatable, Hashable and Codable for structs whose properties conform, and Comparable for enums. Organising a type's conformances in separate extensions (extension Order: Codable {}) keeps code tidy. Protocols can inherit from other protocols and can be restricted to classes with AnyObject, for example for delegates held weakly.

Protocols as plug shapes

Different types plug into the same protocol; extensions add shared behaviour for all of them.

Three differently shaped blocks each fitted with the same connector shape plugging into one socket.
Figure 5.1 — Types conforming to a protocol with a default implementation.

A protocol with a default implementation and synthesised conformances

Conforming types get describe() for free.

protocol Shippable {
    var weightGrams: Int { get }
    var isFragile: Bool { get }
}

extension Shippable {
    var shippingFeePaise: Int {                       // default implementation
        let base = weightGrams <= 500 ? 4_900 : 9_900
        return isFragile ? base + 2_500 : base
    }
}

struct Book: Shippable, Hashable, Codable {           // Hashable and Codable synthesised
    let isbn: String
    let title: String
    let weightGrams: Int
    var isFragile: Bool { false }
}

struct VaseOrder: Shippable {
    let weightGrams: Int
    let isFragile = true
}

extension Double {
    var formattedINR: String { formatted(.currency(code: "INR")) }   // extend an existing type
}

let items: [any Shippable] = [Book(isbn: "978-0", title: "Swift", weightGrams: 400), VaseOrder(weightGrams: 900)]
let totalFee = items.reduce(0) { $0 + $1.shippingFeePaise }
print(Double(totalFee / 100).formattedINR)

Conformance in extensions

Writing extension Order: Codable { ... } separately from the main type groups each protocol's requirements together, making large types far easier to navigate.

Quick check: How can a protocol provide behaviour shared by all conforming types?

  • Through a protocol extension with default implementations
  • Through inheritance only
  • By making the protocol a class
  • It cannot
Answer

Through a protocol extension with default implementations — Protocol extensions supply default implementations that conforming types inherit.