Section 2 · Modelling with Types
Protocols and Extensions
16 min3 min readSpot the bug
Lesson at a glance
Define protocols as contracts, adopt standard ones like Identifiable and Hashable, add default behaviour with protocol extensions, and extend types you don’t own.
- 1Writing a protocol
- 2Standard protocols you’ll use every day
- 3Default behaviour with protocol extensions
- 4some and any
- 5Extending types you don’t own
- 6Where you’ll meet protocols next
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Pocket Budget is about to grow a second kind of cost. Next to one-off expenses there are subscriptions, billed monthly or yearly. The summary screen shouldn’t care which is which: it needs a title and a monthly cost from each. Inheritance would force both into a class hierarchy. Swift’s answer is a protocol: a list of requirements any type can promise to meet, whether it’s a struct, a class or an enum.
Writing a protocol
import Foundation
protocol BudgetItem {
var title: String { get }
var monthlyCost: Decimal { get }
}{ get } means “readable”. A conforming type can satisfy it with a stored property or a computed one. Here are two conforming types, building on the Category enum from the previous lesson:
struct Expense: Identifiable, Hashable, BudgetItem {
let id = UUID()
var title: String
var amount: Decimal
var category: Category
var date: Date
var monthlyCost: Decimal { amount }
}
struct Subscription: BudgetItem {
var title: String
var price: Decimal
var billedYearly: Bool
var monthlyCost: Decimal {
billedYearly ? price / 12 : price
}
}Leave out monthlyCost and the compiler stops you with “type ‘Subscription’ does not conform to protocol ‘BudgetItem’”, and Xcode offers to add stubs for whatever is missing.
Standard protocols you’ll use every day
Expense also adopts two protocols from the standard library. Identifiable requires an id property, and SwiftUI’s List and ForEach use it to tell rows apart, so you’ll see it on nearly every model in this course. Hashable lets expenses go into a Set or be dictionary keys, and it includes Equatable, which gives you ==.
You didn’t write == or a hash function. For structs and enums, Swift synthesizes Equatable and Hashable when every stored property already conforms, and UUID, String, Decimal, Date and Category all do. Because id is part of that comparison, two expenses with identical titles and amounts are still different expenses, which is what the user means.
Default behaviour with protocol extensions
Every budget item needs a formatted price. Instead of writing it twice, extend the protocol:
extension BudgetItem {
func formattedCost(currencyCode: String = "USD") -> String {
monthlyCost.formatted(.currency(code: currencyCode))
}
}
let coffee = Expense(title: "Coffee", amount: 4.5, category: .food, date: .now)
let music = Subscription(title: "Music", price: 120, billedYearly: true)
coffee.formattedCost() // "$4.50"
music.formattedCost(currencyCode: "EUR") // "€10.00"Every type that conforms to BudgetItem, now or in the future, gets formattedCost for free. This is the core of what people mean by protocol-oriented Swift: small protocols, with shared behaviour in extensions, adopted by value types.
some and any
Two keywords describe “a value of some type that conforms”:
let items: [any BudgetItem] = [coffee, music]
let monthly = items.reduce(0) { $0 + $1.monthlyCost } // 14.5
func cheapest(_ list: [some BudgetItem]) -> String? {
list.min { $0.monthlyCost < $1.monthlyCost }?.title
}any BudgetItem is a box that can hold a different conforming type in each slot, which is what a mixed list needs. some BudgetItem means “one specific conforming type, decided by the caller”: cheapest works with [Expense] or [Subscription], but not with a mix. Prefer some when it fits, because the compiler knows the exact type and can optimize. SwiftUI’s var body: some View uses the same keyword in return position, where the roles flip: the view’s implementation picks the one concrete type, and callers only know it’s a View.
Extending types you don’t own
Extensions aren’t limited to protocols. You can add computed properties, methods and conformances to any type, including Apple’s:
extension Decimal {
var asCurrency: String { formatted(.currency(code: "USD")) }
}
extension Expense: CustomStringConvertible {
var description: String { "\(title) (\(category.rawValue)): \(formattedCost())" }
}
print(coffee) // Coffee (food): $4.50Many Swift teams also use extensions to organise a file: the type’s stored properties at the top, then one extension per conformance. One limit applies everywhere: an extension can’t add stored properties, only computed ones. Also, when an extension method on a struct changes a property, it still needs mutating, exactly like a method in the original declaration.
Where you’ll meet protocols next
Protocols are how Apple’s frameworks talk to your code. In SwiftUI, every screen you build is a struct that conforms to the View protocol, whose single requirement is a body property. List asks for Identifiable elements so it can track rows. Networking code asks for Codable so JSON can become your types. Swift 6 concurrency asks for Sendable before a value may cross between threads. Each time, the pattern is the one from this lesson: the framework states a contract, your type promises to meet it, and the compiler checks the promise.
You now have the full modelling toolkit: structs, classes, enums and protocols. The last piece before SwiftUI is what happens when an operation fails, such as an expense that would blow the budget, and how Swift’s error handling makes failure part of a function’s type.
Key takeaways
- A protocol lists requirements; any struct, class or enum that implements them can be used wherever the protocol is expected.
- For structs and enums, Swift synthesizes
EquatableandHashableautomatically when every stored property already conforms. - A protocol extension gives every conforming type a default implementation for free.
- Extensions add methods, computed properties and conformances to any type, including ones from Apple, but never new stored properties.
some Protocolstands for one specific conforming type (for a parameter, the caller picks it);any Protocolis a box that can hold a different conforming type each time.