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¶
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
anyin[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
Selfor associated-type requirements used as plain types — those need generics orany(next lessons).
Exercises¶
- Define
protocol Animal { var name: String { get }; func speak() -> String }and three conforming types; store them in[any Animal]. - Add a default
describe()toAnimalvia an extension, and override it in one type. - Extend
Array where Element: ComparablewithisSorted() -> Bool. - Make a
struct Fibonacci: Sequencewith its own iterator, then print the first 10 values with.prefix(10). - Conform a
Temperaturestruct toComparableandCustomStringConvertible; sort an array of them. - Define a
Storageprotocol (save(_:),load()), writeInMemoryStorageandFileStorage, 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 →