Protocols
Protocols are Swift’s principal abstraction mechanism — declarations of method, property, initialiser, and subscript requirements that conforming types must satisfy. The conventional Swift discipline is protocol-oriented programming (POP) — designing through protocols with default implementations (via extensions) rather than through class hierarchies. Protocols may have associated types, Self requirements, default implementations, and may be composed (A & B). Conformance is explicit — types must declare conformance via the colon syntax. The standard library is built on protocols (Sequence, Collection, Equatable, Hashable, Codable, Comparable, CustomStringConvertible); custom types gain substantial functionality by conforming. The combination — explicit conformance, default implementations via extensions, associated types for type-level relationships, the Self keyword, the standard-library protocol-oriented design — is the substance of Swift’s protocol mechanism.
Protocol declarations
protocol Greetable {
var name: String { get } // required property
func greet() -> String // required method
}
struct Person: Greetable {
let name: String
func greet() -> String {
"Hello, \(name)"
}
}
let p = Person(name: "Alice")
print(p.greet()) // "Hello, Alice"
The form: protocol Name { requirements }. Conforming types must implement all requirements.
Property requirements
Properties may be get (readable) or get set (readable and writable):
protocol Configurable {
var name: String { get } // readable; may be let or var
var value: Int { get set } // must be var (read and write)
}
struct Setting: Configurable {
let name: String // satisfies get-only with let
var value: Int // satisfies get set with var
}
For static properties (type-level):
protocol VersionInfo {
static var version: String { get }
}
Method requirements
protocol Animal {
func makeSound() -> String
mutating func feed(_ food: Food) // mutating allowed in struct conformance
}
struct Cat: Animal {
var hunger = 100
func makeSound() -> String { "Meow" }
mutating func feed(_ food: Food) { hunger -= 10 }
}
The mutating is required in protocol method requirements that mutate self for value types; the same method may be non-mutating for class conformances.
Initialiser requirements
protocol Default {
init()
}
struct Empty: Default {
init() { }
}
class Origin: Default {
required init() { } // class conformance requires `required`
}
The required admits subclasses to inherit the conformance.
Subscript requirements
protocol Lookup {
subscript(key: String) -> String? { get }
}
struct Dictionary: Lookup {
private var data: [String: String] = [:]
subscript(key: String) -> String? { data[key] }
}
Protocol inheritance
Protocols may inherit from other protocols:
protocol Greetable {
var name: String { get }
func greet() -> String
}
protocol Formal: Greetable { // inherits from Greetable
func formalGreet() -> String
}
struct Person: Formal {
let name: String
func greet() -> String { "Hi, \(name)" }
func formalGreet() -> String { "Good day, \(name)" }
}
Conformance to Formal requires conformance to Greetable (and all its requirements).
Default implementations via extensions
Protocols may provide default implementations — methods that conformers can use without explicit definition:
protocol Describable {
var name: String { get }
func describe() -> String
}
extension Describable {
func describe() -> String { // default implementation
"I am \(name)"
}
}
struct Person: Describable {
let name: String
// describe() is provided by the extension
}
let p = Person(name: "Alice")
p.describe() // "I am Alice"
The mechanism admits substantial code reuse via the protocol-oriented programming paradigm.
For overriding the default:
struct Robot: Describable {
let name: String
func describe() -> String { // override the default
"Robot designation: \(name)"
}
}
Constrained extensions
Default implementations may be conditional on additional constraints:
protocol Container {
associatedtype Item
var items: [Item] { get }
}
extension Container where Item: Equatable {
func contains(_ target: Item) -> Bool {
items.contains { $0 == target }
}
}
struct IntBag: Container {
let items: [Int]
}
IntBag(items: [1, 2, 3]).contains(2) // true
The where admits substantial conditional functionality — contains is admitted only when Item: Equatable.
Protocol composition
The & admits “conform to multiple protocols”:
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
func describe(_ subject: Named & Aged) -> String {
"\(subject.name) (\(subject.age))"
}
struct Person: Named, Aged {
let name: String
let age: Int
}
print(describe(Person(name: "Alice", age: 30))) // "Alice (30)"
The Named & Aged admits a value satisfying both protocols.
For substantial use cases, declaring an explicit composite protocol may be conventionally clearer:
protocol Person: Named, Aged { }
func describe(_ subject: Person) { /* ... */ }
Associated types
Treated more substantially in Generics.
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
var items: [Int] = []
var count: Int { items.count }
mutating func append(_ item: Int) { items.append(item) }
subscript(i: Int) -> Int { items[i] }
}
Self requirements
The Self keyword admits “the conforming type”:
protocol Cloneable {
func clone() -> Self
}
struct Point: Cloneable {
let x: Int
let y: Int
func clone() -> Self { // Self = Point
Point(x: x, y: y)
}
}
The mechanism admits fluent APIs returning the conforming type.
Standard-library protocols
The substantial protocols built into the standard library:
Equatable
protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
}
struct Point: Equatable {
let x: Int
let y: Int
// == is synthesised for structs whose properties are all Equatable
}
Point(x: 1, y: 2) == Point(x: 1, y: 2) // true
Hashable
protocol Hashable: Equatable {
func hash(into hasher: inout Hasher)
}
struct Person: Hashable {
let name: String
let age: Int
// hash is synthesised
}
let set: Set<Person> = [Person(name: "Alice", age: 30)]
Comparable
protocol Comparable: Equatable {
static func < (lhs: Self, rhs: Self) -> Bool
}
struct Distance: Comparable {
let meters: Double
static func < (lhs: Distance, rhs: Distance) -> Bool {
lhs.meters < rhs.meters
}
// <=, >, >= derived from <
}
CustomStringConvertible
protocol CustomStringConvertible {
var description: String { get }
}
struct Point: CustomStringConvertible {
let x: Int
let y: Int
var description: String { "(\(x), \(y))" }
}
print(Point(x: 1, y: 2)) // "(1, 2)"
Codable
typealias Codable = Encodable & Decodable
struct User: Codable {
let id: Int
let name: String
let email: String
// encoding/decoding synthesised automatically
}
let json = try JSONEncoder().encode(user)
let user = try JSONDecoder().decode(User.self, from: json)
Sequence and Collection
protocol Sequence {
associatedtype Element
associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
func makeIterator() -> Iterator
}
protocol Collection: Sequence {
associatedtype Index: Comparable
var startIndex: Index { get }
var endIndex: Index { get }
subscript(position: Index) -> Element { get }
func index(after i: Index) -> Index
}
These are the foundation of Swift’s iteration; conforming admits the substantial Sequence/Collection method library.
Protocol-oriented programming (POP)
The conventional Swift design philosophy — protocol-oriented programming:
- Define behaviour through protocols.
- Provide default implementations via extensions.
- Use protocol composition for substantial requirements.
- Use generic constraints (
<T: Protocol>) for type-safe abstraction. - Use
someandanyfor runtime polymorphism.
// Behaviour:
protocol Logger {
func log(_ message: String)
}
protocol Timestamped {
var timestamp: Date { get }
}
// Defaults:
extension Logger where Self: Timestamped {
func log(_ message: String) {
print("[\(timestamp)] \(message)")
}
}
// Composition:
struct AppLogger: Logger, Timestamped {
var timestamp: Date { Date.now }
// log() provided by extension
}
Protocols vs classes for inheritance
The conventional Swift discipline favours protocols over class inheritance:
| Approach | Example |
|---|---|
| Class inheritance | class Dog: Animal { ... } |
| Protocol conformance | struct Dog: Animal { ... } |
Protocols admit:
- Multiple conformance — a type may conform to several protocols.
- Value types — structs (preferred) admit conformance.
- Default implementations via extensions.
- Retroactive conformance — adding conformance to types from other modules.
Class inheritance is reserved for genuine reference-semantic hierarchies.
Common patterns
Conformance
struct Person: Equatable, Hashable, Codable, CustomStringConvertible {
let id: Int
let name: String
let age: Int
var description: String {
"Person(id: \(id), name: \(name), age: \(age))"
}
// Equatable, Hashable, Codable synthesised
}
Protocol with default implementation
protocol Trackable {
var id: UUID { get }
func track()
}
extension Trackable {
func track() {
print("Tracking \(id)")
}
}
Protocol composition
typealias Identifiable & Hashable & Codable = StorageKey
func store<K: StorageKey, V>(_ value: V, for key: K) {
/* ... */
}
Standard-library protocol-driven function
func sum<S: Sequence>(_ s: S) -> S.Element
where S.Element: Numeric {
s.reduce(0, +)
}
sum([1, 2, 3]) // 6
sum(1...100) // 5050
sum(stride(from: 0, to: 10, by: 2)) // 20
Custom Comparable
struct Version: Comparable {
let major, minor, patch: Int
static func < (lhs: Version, rhs: Version) -> Bool {
(lhs.major, lhs.minor, lhs.patch) < (rhs.major, rhs.minor, rhs.patch)
}
}
let versions = [Version(major: 1, minor: 2, patch: 0),
Version(major: 1, minor: 1, patch: 5)]
versions.sorted() // ascending
versions.max() // 1.2.0
Protocol with associated type
protocol DataSource {
associatedtype Item
func numberOfItems() -> Int
func item(at index: Int) -> Item
}
struct UserDataSource: DataSource {
private let users: [User]
func numberOfItems() -> Int { users.count }
func item(at index: Int) -> User { users[index] }
}
Mock for testing via protocol
protocol UserService {
func find(_ id: Int) async throws -> User?
func save(_ user: User) async throws
}
class RealUserService: UserService {
func find(_ id: Int) async throws -> User? { /* network */ }
func save(_ user: User) async throws { /* network */ }
}
class MockUserService: UserService {
var users: [Int: User] = [:]
func find(_ id: Int) async throws -> User? { users[id] }
func save(_ user: User) async throws { users[user.id] = user }
}
The pattern admits substantial dependency injection.
Protocol for retroactive functionality
extension String: Greetable {
var name: String { self }
func greet() -> String { "Hello, \(self)" }
}
"Alice".greet() // "Hello, Alice"
The mechanism admits adding conformances to types from other modules — retroactive conformance.
RawRepresentable for enums with raw values
enum Status: String, RawRepresentable {
case active
case inactive
case banned
}
let s = Status(rawValue: "active") // Optional(.active)
print(s!.rawValue) // "active"
The conformance admits substantial conversion between raw values and enum cases.
Static methods in protocols
protocol Persisted {
associatedtype Identifier
static func find(_ id: Identifier) async throws -> Self?
}
struct User: Persisted {
typealias Identifier = UUID
static func find(_ id: UUID) async throws -> User? { /* ... */ }
}
A note on the conventional discipline
The contemporary Swift protocols advice:
- Use protocols for abstraction; classes for reference semantics.
- Provide default implementations via extensions where applicable.
- Use protocol composition (
A & B) for substantial requirements. - Use
somefor opaque return types. - Use
anyfor heterogeneous collections. - Conform to standard-library protocols (
Equatable,Hashable,Codable,CustomStringConvertible). - Use generic constraints with protocol bounds.
- Use
Selfrequirements for fluent APIs. - Use retroactive conformance sparingly.
- Write protocols with substantial substance — single-method protocols may not justify the abstraction.
The combination — explicit conformance, default implementations, associated types, Self requirements, protocol composition, the standard-library’s substantial protocol surface, the protocol-oriented programming style — is the substance of Swift’s protocol mechanism. The discipline produces composable, testable, type-safe code with substantial reuse via default implementations.