You've now met three named types: structs, classes, and enums. There's a fourth, and it's a different animal. A struct, class, or enum is something you create instances of. A protocol is something you don't -- it has no instances, no storage, no implementation. It's a contract: a list of capabilities a type promises to provide, with the actual code left entirely to whoever signs it.
Because they describe a shape rather than a thing, protocols are often called abstract types. And you've been leaning on them since your very first program without noticing -- ==, sorting an array, using a value as a dictionary key, printing a value -- every one of those is powered by a protocol in the standard library. This chapter pulls back the curtain on how they work and why they're the backbone of idiomatic Swift.
We'll build the running examples around something this app does constantly: pushing builds and assets to App Store Connect.
Defining a Protocol
You declare a protocol much like any other type -- the keyword, a name, and a body of requirements. Here's the abstract idea of "something that can be uploaded":
protocol Uploadable {
/// A human-readable description of this upload's state.
func summary() -> String
}The body looks like a type with the implementations deleted. summary() has a signature but no { ... } -- the protocol says what must exist, never how. That's why you can't create one:
let thing = Uploadable() // ⛔ 'Uploadable' cannot be constructed; it's a protocolFlesh out the contract. An upload can be pushed forward chunk by chunk and cancelled, and both of those change its state:
protocol Uploadable {
func summary() -> String
/// Send the next chunk of bytes.
mutating func advance()
/// Abort and reset progress to zero.
mutating func cancel()
}You mark advance() and cancel() as mutating because conforming value types will need to modify their own state inside them. (You met mutating on structs back in the Methods chapter; here it appears in the contract so the protocol can be adopted by structs as well as classes.)
Protocols can require properties, too -- but with a twist. You must spell out whether each is gettable or gettable-and-settable:
protocol Uploadable {
func summary() -> String
mutating func advance()
mutating func cancel()
/// Bytes uploaded so far.
var bytesSent: Int { get set }
/// The largest payload this kind of upload accepts.
static var maxBytes: Int { get }
}The { get set } on bytesSent says conformers must make it both readable and writable; { get } on maxBytes requires only that it be readable. Crucially, the protocol makes no claim about storage -- a conformer can satisfy a { get set } requirement with a plain stored property or a computed one with a getter and setter. That's the whole point of an abstract type: it describes the interface, not the implementation. The static on maxBytes marks it as belonging to the type itself, not to any one instance -- every upload of a given kind shares the same ceiling.
Adopting a Protocol
A class, struct, or enum can adopt a protocol; once it implements every requirement, it conforms. The syntax is a colon after the type name -- the same punctuation as class inheritance:
class ResumableUpload: Uploadable {
}As written, that won't compile -- ResumableUpload promised to be Uploadable but delivered nothing. Fill in the requirements:
class ResumableUpload: Uploadable {
var bytesSent = 0
static var maxBytes: Int { 50_000_000 }
func summary() -> String {
"Resumable upload — \(bytesSent) bytes sent"
}
func advance() {
bytesSent = min(bytesSent + 1_000_000, Self.maxBytes)
}
func cancel() {
bytesSent = 0
}
}Notice advance() and cancel() are not marked mutating here, even though the protocol required it and they clearly change bytesSent. That's because ResumableUpload is a class -- a reference type whose methods can always mutate the instance. mutating only matters for value types. (Also note Self.maxBytes -- capital-S Self means "this conforming type," so it reads the right static value.)
Now write the same idea as a struct, but conform it a little differently:
struct OneShotUpload {
var bytesSent = 0
static var maxBytes: Int { 5_000_000 }
func summary() -> String {
"One-shot upload — \(bytesSent) bytes sent"
}
mutating func advance() {
bytesSent = min(bytesSent + 5_000_000, Self.maxBytes)
}
mutating func cancel() {
bytesSent = 0
}
}
extension OneShotUpload: Uploadable {}OneShotUpload has everything Uploadable asks for, but the struct's own declaration doesn't say so. Implementing the requirements isn't enough -- conformance must be declared explicitly. Here it's declared in an extension (extension OneShotUpload: Uploadable {}), with an empty body because the type already satisfies the contract. As a value type, its advance() and cancel() do need mutating.
Adding conformance through an extension is a genuinely useful trick called retroactive modeling: you can make a type conform to a protocol after the fact, even a type whose source you don't own (one from a framework, say). You re-open it and bolt the conformance on.
You can declare methods, computed properties, and conformances in an extension, but not stored properties -- those must live in the type's original definition (or, for a class, a subclass). So retroactive conformance works only when the requirements can be met with computed properties and methods. A protocol that demands new storage can't be satisfied from an extension alone.
Mini-exercise
Define a protocol Estimable with a single read-only requirement, var estimatedBytes: Int { get }. Conform three small structs -- Thumbnail, Trailer, and Manifest -- to it, each returning a different estimate. Put one of each into an [any Estimable] array and compute the total with reduce. (You'll meet any properly in a moment -- for now, read [any Estimable] as "an array of assorted things that are Estimable.")
Default Implementations
Look back at the two conformers: their cancel() methods are byte-for-byte identical. Duplication like that is a smell, and protocols have a cure -- protocol extensions that supply a default implementation:
extension Uploadable {
mutating func cancel() {
bytesSent = 0
}
}Now any type that conforms to Uploadable gets cancel() for free and can delete its own copy. A conformer that needs something extra -- flushing a buffer, releasing a file handle -- can still write its own cancel(), and that version wins. The default is a fallback, not a cage.
Protocol extensions can also add brand-new members that aren't part of the formal contract:
extension Uploadable {
/// Progress as a value from 0.0 to 1.0.
var fractionComplete: Double {
Double(bytesSent) / Double(Self.maxBytes)
}
}Every conforming type now has fractionComplete -- you wrote it once, for all of them. But there's a sharp difference between these two kinds of extension member, and it trips people up:
cancel()is declared in the protocol and given a default in the extension. It's a true requirement, so it's dynamically dispatched -- a conformer's owncancel()overrides the default even when the value is handled as anUploadable.fractionCompleteis only in the extension, never declared as a requirement. It's statically dispatched on the declared type. If a conformer defines its ownfractionComplete, that version is used only when the value is known as that concrete type -- call it throughany Uploadableand you get the extension's version regardless. In effect, extension-only members can't be overridden through the protocol.
The rule of thumb: if you want conformers to be able to customize a behavior, declare it in the protocol. If it's a fixed convenience derived from other requirements, an extension-only member is fine.
Faking Default Parameters
Functions can have default parameter values; protocol requirements cannot. This won't compile:
enum RetryDelay {
case immediate, standard, backoff
}
protocol Retryable {
mutating func retry(after delay: RetryDelay = .standard) // ⛔ default arguments
// not allowed here
}The workaround uses the default-implementation trick you just learned. Require the full method, then add a no-argument convenience in an extension that fills in the default:
protocol Retryable {
mutating func retry(after delay: RetryDelay)
}
extension Retryable {
mutating func retry() {
retry(after: .standard)
}
}A conformer still has to implement retry(after:) for every delay, but it gets the zero-argument retry() -- standard delay assumed -- automatically.
Requiring Initializers
A protocol can't be initialized, but it can demand that conformers provide specific initializers:
protocol APISession {
var token: String { get }
init(token: String)
init?(resuming saved: String)
}Any APISession must offer both a plain initializer and a failable one. When a class conforms, those initializers have to be marked required -- which guarantees every subclass provides them too, keeping the conformance intact down the hierarchy:
class JWTSession: APISession {
let token: String
required init(token: String) {
self.token = token
}
required init?(resuming saved: String) {
guard !saved.isEmpty else { return nil }
self.token = saved
}
}If you marked JWTSession as final, you could drop required -- a class that can't be subclassed has no descendants to enforce the requirement on, so Swift doesn't ask. The failable init?(resuming:) works exactly as it did for enums: it returns nil when the input can't produce a valid instance (here, an empty saved token).
Protocol Inheritance
Uploadable describes any upload. But resumable uploads have something one-shot uploads don't -- a token to pick up where they left off. You can express "an Uploadable, plus more" by having one protocol inherit from another:
protocol ResumableUploadable: Uploadable {
var resumeToken: String { get }
}A type marked ResumableUploadable must satisfy both its own requirement (resumeToken) and everything in Uploadable. Just like class subclassing, this creates an "is-a" relationship: every ResumableUploadable is an Uploadable. Extend the class to opt in:
extension ResumableUpload: ResumableUploadable {
var resumeToken: String { "tok-\(bytesSent)" }
}Using Protocols for Polymorphism
The payoff, same as with class hierarchies, is polymorphism -- write code against the abstract type and it works for every conformer, whether struct, class, or enum. Say you want to cancel a batch of uploads:
func cancelAll(_ uploads: [any Uploadable]) {
uploads.forEach { upload in
upload.cancel() // ⛔ cannot use mutating member on immutable value
}
}The compiler stops you. cancel() is mutating, and forEach hands its closure an immutable copy of each element -- you can't mutate it. Because some conformers are value types, Swift won't let you mutate one through an immutable binding. Reach for the array's indices and an inout parameter instead:
func cancelAll(_ uploads: inout [any Uploadable]) {
for index in uploads.indices {
uploads[index].cancel()
}
}Two things changed. inout makes the array itself modifiable, so mutating its elements is allowed. And instead of iterating the elements directly, you iterate the indices and reach back into the array by subscript -- uploads[index] is a mutable slot, so calling a mutating method on it succeeds.
Notice the type: [any Uploadable]. The keyword any marks an existential -- a box that can hold any concrete type conforming to Uploadable, with its real type erased at compile time. Writing any Uploadable (rather than bare Uploadable) makes that box explicit, and it signals a small runtime cost: the box has to store and dynamically dispatch through whatever's inside. Modern Swift wants you to be deliberate about that cost, so any is the right habit -- and in some cases the compiler now requires it.
Associated Types
Sometimes a protocol needs to refer to a type it can't pin down in advance -- one that each conformer chooses for itself. An asset package, for instance, carries a manifest, but a screenshot package's manifest (a list of filenames) looks nothing like a binary package's (a single checksum). Declare the unknown with associatedtype:
protocol Packageable {
associatedtype Manifest
var manifest: Manifest { get }
}Manifest is a placeholder. The protocol says "there's some manifest type here," and leaves the choice to each conformer:
struct ScreenshotPackage: Packageable {
var manifest: [String] // Manifest is inferred as [String]
}
struct BinaryPackage: Packageable {
typealias Manifest = Int // or state it explicitly
var manifest: Int
}ScreenshotPackage lets Swift infer Manifest from the type of manifest; BinaryPackage spells it out with typealias (usually unnecessary, but allowed when you want it explicit). The two conformers now have genuinely different contracts -- the meaning of Packageable flexes per adopter.
That flexibility has a cost: because the manifest type isn't known up front, you can't use Packageable as a plain variable type. You must wrap it in any:
let packages: [any Packageable] = [
ScreenshotPackage(manifest: ["home.png", "detail.png"]),
BinaryPackage(manifest: 0xC0FFEE),
]For protocols without associated types, any is encouraged but optional. For protocols with them, it's required -- the existential box is doing real work to hide the differing underlying types behind one interface.
Conforming to Many Protocols
A class can inherit from only one class. But any type -- class, struct, or enum -- can conform to as many protocols as you like. That's Swift's answer to the limits of single inheritance. Split "can be chunked" into its own small protocol:
protocol Chunked {
var chunkCount: Int { get }
var chunkSize: Int { get }
}
extension ResumableUpload: Chunked {
var chunkCount: Int { 50 }
var chunkSize: Int { 1_000_000 }
}ResumableUpload now conforms to Uploadable, ResumableUploadable, and Chunked -- a kind of multiple inheritance that subclassing could never give you, because each protocol contributes an independent set of capabilities.
When a class both subclasses another class and adopts protocols, the superclass comes first in the list, then the protocols: class FastResumableUpload: ResumableUpload, Chunked, CustomStringConvertible. Structs and enums have no superclass, so they just list protocols.
Composition, any, and some
Sometimes a function needs a value that satisfies several protocols at once. Combine them with the & composition operator. Suppose you want to abort a transfer and report how many chunks it spanned -- you need both Uploadable (for cancel()) and Chunked (for chunkCount):
func abort(_ transfer: inout any Uploadable & Chunked) {
transfer.cancel()
print("Aborted across \(transfer.chunkCount) chunks.")
}any Uploadable & Chunked is an existential box requiring both conformances. But calling it reveals an awkwardness:
var upload: any Uploadable & Chunked = ResumableUpload()
abort(&upload) // works — but `upload` must be declared as the existential typeTo mutate an existential through inout, the variable has to be declared as exactly that existential type. You can't pass a plain ResumableUpload: Swift would box it into a temporary any Uploadable & Chunked, mutate the box, and throw it away -- leaving your original untouched. (Helpfully, it's a compile error rather than a silent bug.)
The cleaner fix swaps any for some:
func abort(_ transfer: inout some Uploadable & Chunked) {
transfer.cancel()
print("Aborted across \(transfer.chunkCount) chunks.")
}
var upload = ResumableUpload() // no existential annotation needed
abort(&upload)some Uploadable & Chunked is not a box. It tells the compiler to generate a specialized version of abort for each concrete type that's both Uploadable and Chunked -- abort becomes a fully generic function. No box, no erasure, no inout-existential headache. The distinction in one line: any is one boxed type that can hold many; some is many specialized copies, one per concrete type. That some quietly turns a function generic is your first glimpse of how thoroughly protocols and generics are wired together -- the subject of the next chapter.
When You Need Reference Semantics
Protocols can be adopted by value types and reference types alike, so what semantics does a protocol-typed variable have -- value or reference? The honest answer: whichever the concrete type underneath has. A small demonstration with an editable draft:
protocol Editable {
var draftTitle: String { get set }
}
class SharedDraft: Editable {
var draftTitle: String
init(draftTitle: String) { self.draftTitle = draftTitle }
}
struct DraftSnapshot: Editable {
var draftTitle: String
}Put a class instance behind the protocol type and you get reference semantics -- the copy is an alias:
var editable: any Editable = SharedDraft(draftTitle: "Working title")
var copy = editable
editable.draftTitle = "Final title"
editable.draftTitle // "Final title"
copy.draftTitle // "Final title" — same objectPut a struct instance behind the very same variable and you get value semantics -- the copy is independent:
editable = DraftSnapshot(draftTitle: "Working title")
copy = editable
editable.draftTitle = "Final title"
editable.draftTitle // "Final title"
copy.draftTitle // "Working title" — separate valueWhen a protocol is meant to be adopted only by classes -- and you want to rely on reference semantics -- say so by constraining it to AnyObject:
protocol Editable: AnyObject {
var draftTitle: String { get set }
}Now only classes can conform, and the reference behavior is guaranteed. (An older class keyword does the same job, but AnyObject is the preferred spelling today.)
Protocols Are More Than Bags of Syntax
A protocol checks shape: the methods, properties, and initializers a conformer must provide. What it can't check is meaning. Your Uploadable contract says cancel() must exist and be mutating -- but nothing in the language forces cancel() to actually reset bytesSent to zero. A conformer could implement it to do nothing, or to increase the count, and still compile. Likewise a protocol might require an operation to be O(1), or a property to never return an empty string -- guarantees that live only in documentation and the conformer's good faith.
This is why people say protocols are "more than bags of syntax." The compiler verifies the signatures; you are responsible for honoring the intent behind them. And it's exactly why Swift makes you declare conformance explicitly instead of inferring it from matching method names -- writing : Uploadable is you signing the contract, promising you've met not just its syntax but its meaning.
Protocols in the Standard Library
Here's where it pays off. Swift's standard library is built out of protocols, and conforming your own types to them unlocks piles of built-in behavior. A few you'll use constantly:
Equatable
You compare Ints and Strings with == every day. That isn't magic reserved for built-in types -- those types simply conform to Equatable, whose entire contract is one static operator:
protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
}Teach your own type what equality means by implementing it. A semantic version is equal to another when all three components match:
struct SemanticVersion {
let major: Int
let minor: Int
let patch: Int
}
extension SemanticVersion: Equatable {
static func == (lhs: SemanticVersion, rhs: SemanticVersion) -> Bool {
lhs.major == rhs.major && lhs.minor == rhs.minor && lhs.patch == rhs.patch
}
}
SemanticVersion(major: 1, minor: 4, patch: 7)
== SemanticVersion(major: 1, minor: 4, patch: 7) // trueFor a struct or enum whose every stored property (or associated value) is already Equatable, Swift synthesizes == for you -- just declare conformance (struct SemanticVersion: Equatable {}) and skip the implementation. You only hand-write == for classes, or when "equal" means something other than "all fields match." The manual version above is shown so you can see what the synthesis is doing for you.
Comparable
Comparable refines Equatable, adding the ordering operators. In practice you implement just one of them -- < -- and the standard library derives <=, >, and >= from your < and ==:
extension SemanticVersion: Comparable {
static func < (lhs: SemanticVersion, rhs: SemanticVersion) -> Bool {
(lhs.major, lhs.minor, lhs.patch) < (rhs.major, rhs.minor, rhs.patch)
}
}(That tuple comparison sorts by major, then minor, then patch -- exactly how versions order.) Conform to Comparable and a whole arsenal of "free" collection methods lights up, because the standard library now knows how to order your values:
var releases = [
SemanticVersion(major: 2, minor: 1, patch: 0),
SemanticVersion(major: 1, minor: 9, patch: 3),
SemanticVersion(major: 2, minor: 1, patch: 1),
]
releases.sort() // 1.9.3, 2.1.0, 2.1.1
releases.max() // 2.1.1
releases.min() // 1.9.3
releases.contains(SemanticVersion(major: 1, minor: 9, patch: 3)) // trueYou implemented one operator; you got sorting, min, max, and more.
Hashable
Hashable (a refinement of Equatable) is the ticket to using a value as a dictionary key or a set member. Value types get it synthesized, but for a class you write it by hand -- and the golden rule is that whatever you compare in ==, you must also feed to the hasher:
class BetaTester {
let email: String
let name: String
let group: String
init(email: String, name: String, group: String) {
self.email = email
self.name = name
self.group = group
}
}
extension BetaTester: Hashable {
static func == (lhs: BetaTester, rhs: BetaTester) -> Bool {
lhs.email == rhs.email && lhs.name == rhs.name && lhs.group == rhs.group
}
func hash(into hasher: inout Hasher) {
hasher.combine(email)
hasher.combine(name)
hasher.combine(group)
}
}hash(into:) hands each property to the passed-in Hasher, which does the actual mixing. Keeping == and hash(into:) in sync matters: two values that are equal must produce the same hash, or dictionaries and sets misbehave. With that in place, a BetaTester can key a dictionary:
let tester = BetaTester(email: "ada@example.com", name: "Ada", group: "Internal")
let buildInvites = [tester: "Build 412"]Identifiable
Identifiable requires just one thing: a get-only id property whose type is Hashable. It's how SwiftUI tells list rows apart, among other uses. Pick a property that's genuinely unique per instance:
extension BetaTester: Identifiable {
var id: String { email }
}email works because no two testers share one (and String is Hashable). You wouldn't use name -- two testers could both be "Ada."
CustomStringConvertible
Print a type with no special handling and you get something unhelpful:
print(tester) // a vague, default descriptionConform to CustomStringConvertible -- a single description property -- to control how your type appears in print() and string interpolation:
extension BetaTester: CustomStringConvertible {
var description: String { "\(name) <\(email)>" }
}
print(tester) // Ada <ada@example.com>Its sibling CustomDebugStringConvertible adds a debugDescription surfaced by debugPrint(), handy for richer output that only shows up while you're debugging.
Challenge
Challenge 1: a release-pipeline task system
Model the jobs a release pipeline runs on its artifacts using a family of protocols -- this exercises composition and protocol-typed arrays together. The duties:
- Every artifact must be validated.
- Image artifacts (screenshots, icons) must be rendered to PNG.
- Video artifacts must be transcoded.
- Oversized artifacts must be compressed.
- Anything shipped must be published.
Then:
- Define protocols
Validatable,Renderable,Transcodable,Compressible, andPublishable, each with one method (aprint()body is fine). Create typesScreenshot,AppIcon,AppPreview, andBinary, and have each adopt the protocols that fit it -- e.g.ScreenshotisValidatable & Renderable & Compressible & Publishable, whileAppPreviewisValidatable & Transcodable & Compressible & Publishable. - Build homogeneous arrays typed by protocol --
var renderables: [any Renderable],var transcodables: [any Transcodable], and so on -- and add the artifacts that belong in each. - Loop over each array and call the matching job on every element.
Pay attention to which arrays a given artifact lands in: that membership is the design. An AppPreview should appear in transcodables and compressables but never in renderables, and the compiler enforces it.
Key Points
- A protocol defines a contract -- methods, properties, initializers -- that classes, structs, and enums can adopt. It has no instances and no implementation of its own; it's an abstract type.
- Property requirements specify
{ get }or{ get set }; method requirements that mutate value types are markedmutating; type-level requirements usestatic. - Conformance must be declared explicitly (
: Protocol), even when a type already implements every requirement. You can declare it in the type or, retroactively, in an extension -- though extensions can't add stored properties. - Protocol extensions provide default implementations. A member declared in the protocol is overridable by conformers; a member existing only in an extension is statically dispatched and effectively can't be overridden through the protocol.
- Protocols can inherit from other protocols, and a single type can conform to many protocols -- a flexibility single class-inheritance can't offer. Combine requirements at a use site with the
&composition operator. associatedtypelets a protocol defer a type choice to each conformer; such protocols must be used asanywhen used as a type.any Protocolis an existential box -- one type, many possible contents, with a small runtime cost.some Protocolis opaque/generic -- the function specializes per concrete type.anyboxes;somespecializes.- Constrain a protocol to
AnyObjectwhen only classes should adopt it and you want guaranteed reference semantics. - The compiler checks a protocol's syntax, not its meaning -- honoring the intent behind the requirements is on you, which is why conformance is explicit.
- The standard library is built on protocols:
Equatable(==),Comparable(<, plus freesort/min/max),Hashable(dictionary keys and sets),Identifiable(a uniqueid), andCustomStringConvertible(description). Value types often getEquatable/Hashablesynthesized; classes implement them by hand.
You keep brushing up against one idea from every angle now -- some turning a function generic, associatedtype deferring a type, the standard library writing one sort() that works for every Comparable. That idea is generics: code written once that works across many types, with protocols as the constraints that keep it honest. Protocols were the contract; generics are what you build on top of them. That's the next chapter, and it's where Swift's type system finally clicks into one picture.
Ship your apps faster
When you're ready to publish your Swift app to the App Store, Simple App Shipper handles metadata, screenshots, TestFlight, and submissions — all in one place.
Try Simple App Shipper