A protocol declares a set of capability requirements. A structure, enumeration, or class conforms by supplying matching implementations, so callers can depend on a contract instead of a concrete type.
A method added only by a protocol extension doesn’t become a dynamically replaceable witness. Replacing a generic boundary with any Protocol also erases type relationships that callers may need.
Put dynamically dispatched members in the protocol declaration, then choose generics, some, or any according to ownership: the caller chooses the type, the implementation chooses one type, or storage accepts any conformer at runtime.
What it is and why it exists
A Swift protocol is a capability contract. It can require instance or type properties, methods, initializers, and subscripts without prescribing whether a conformer uses stored properties, computed properties, or another internal design. Structures, enumerations, and classes can all declare protocol conformance , so abstraction doesn’t depend on class inheritance.
Conformance is a named, explicit relationship rather than structural matching by member shape. Even if a type happens to have every same-named member, it must state the protocol name in its declaration or an extension before it can be used as a conformer.
The explicit declaration gives the compiler one place to check and lets readers search for types that promise the contract. A type whose members merely share names but have different semantics isn’t silently accepted.
Protocols answer “what can the caller do?” instead of “where does this object sit in an inheritance hierarchy?” A sending function that only needs a value to produce a message doesn’t need to know whether the value is a structure, actor, or test double. This narrows an API boundary and lets the compiler check the contract when an implementation is replaced.
You encounter protocols throughout the standard library in Sequence, Collection, Equatable, Hashable, Codable, and Sendable. Application code often uses them to isolate storage, networking, clocks, and logging, or to give several value types the same algorithm entry point.
A protocol isn’t an automatic switch for reuse or decoupling. An oversized protocol forces conformers and test doubles to implement unrelated capabilities; a tiny protocol with no stable consumer adds names and navigation. Start with the operations a caller actually needs, then decide whether that boundary deserves a protocol.
How it works
Each property, method, initializer, or subscript in a protocol declaration is a protocol requirement . A conforming type must provide a visible, type-matching implementation or use a default supplied by a protocol extension. The compiler checks completeness where conformance is declared instead of waiting for a missing member at runtime.
A property requirement describes read or read-write capability only. { get } can be satisfied by a constant stored property, variable stored property, or computed property; { get set } can’t be satisfied by a read-only implementation. A type-property requirement is written with static in the protocol, and a class can provide the matching member using an allowed implementation form.
When a value-type method needs to change self or an instance property, the protocol requirement must be marked mutating. Structure and enumeration implementations also use mutating, while class implementations don’t. When a protocol requires an initializer, a non-final class generally uses required so subclasses still provide that construction entry point.
Inheritance and class-only protocols
A protocol can inherit multiple protocols, collecting their requirements into a new long-term contract. A type conforming to the child protocol must satisfy the whole inheritance chain. This isn’t implementation inheritance: the protocol still stores no instance state and supplies no superclass object.
Adding AnyObject to the inheritance list restricts conformance to classes. Use that restriction only when the contract depends on reference semantics, weak references, or object identity. Adding AnyObject mechanically to imitate a class interface needlessly excludes structures and enumerations.
A class can also appear in a protocol inheritance list as a superclass requirement, making every conformer part of that class hierarchy. This is stronger than AnyObject and fits only a boundary whose default implementation must call behavior from that superclass.
Default implementations and dispatch
A protocol extension can implement requirements already declared by the protocol, and it can add convenience members. Those cases dispatch differently: an implementation of a requirement participates in conformance, while an extension-only member is selected from the static type visible at the call site and doesn’t become a replaceable protocol witness.
Putting an algorithm in an extension works well when it is valid for every conformer. If conformers should be able to change behavior observed through a protocol value, declare the member in the protocol first and provide its default in an extension. Merely writing a same-named concrete member doesn’t turn the extension member into a requirement.
Three abstraction boundaries
A protocol name can appear in several type contexts, but those contexts aren’t equivalent. A generic constraint lets the caller choose a concrete type; the opaque type some P lets an implementation choose one hidden concrete type; the existential type any P uses a boxed value to hold some conforming type at runtime.
| Spelling | Who chooses the concrete type | Relationship retained | Typical use |
|---|---|---|---|
<T: P> or parameter some P | Caller | T stays one type within the call | Algorithm parameters, static type relationships |
Return some P | Implementation | Hidden but fixed underlying type | Hide a factory or composed result |
any P | Runtime assigner | Only the current value’s conformance to P | Heterogeneous collections, replaceable storage slots |
The flexibility of any P comes from type erasure and a level of indirection when needed. An existential exposes only members guaranteed by the protocol; concrete-type APIs require a runtime cast. Don’t replace every generic parameter with any just because the signature looks shorter, because same-type relationships between associated types may disappear.
Associated types and composition
An associated type is a type placeholder declared by a protocol and fixed by each conforming type. It expresses relationships such as “a container has one element type” or “a parser produces one output type.” A generic function can then relate Store.Value to a parameter or return value.
A primary associated type is named in angle brackets after the protocol name, but it must still be declared with associatedtype in the body. It lets some Feed<String> and any Feed<String> constrain the associated type concisely; it doesn’t turn the protocol into an ordinary generic type.
A protocol composition P & Q requires one value to satisfy both contracts, but it doesn’t declare a new protocol. Declare a real protocol when the combination needs a name, inheritance, or long-term evolution. Use P & Q when a local parameter simply needs the intersection of two capabilities.
Examples
These three standalone programs progress from basic requirements and defaults to extension-member dispatch and associated types across some and any. This environment has no Swift toolchain, so each block carries the required not-executed marker; the shown output is the expected result of these deterministic examples.
Defining a minimal capability
Resettable requires only a summary and a reset operation; recording a query remains a capability of SearchHistory. The extension derives isEmpty from the protocol requirement so every conformer can reuse that logic.
// # not executed here: Swift toolchain is not installed.
protocol Resettable {
var summary: String { get }
mutating func reset()
}
extension Resettable {
var isEmpty: Bool { summary.isEmpty }
}
struct SearchHistory: Resettable {
private var queries: [String] = []
var summary: String { queries.joined(separator: ",") }
mutating func record(_ query: String) {
queries.append(query)
}
mutating func reset() {
queries.removeAll()
}
}
var history = SearchHistory()
history.record("swift")
history.record("protocols")
print(history.summary)
print(history.isEmpty)
history.reset()
print(history.isEmpty)swift,protocols
false
truemutating is part of the protocol contract; without it, a value-type implementation can’t change itself. The history binding must also be declared with var before code can call a potentially mutating requirement. A class conformer implements the same requirement without writing mutating.
The extension’s isEmpty reads only summary, so it doesn’t know how the query array is stored. Another conformer could calculate its summary from a database count or remote state as long as it satisfies the same public contract.
Observing static selection for extension members
headline() is a protocol requirement, while category() exists only in the extension. The concrete value sees Incident.category(), but the existential finds the extension version through its static interface.
// # not executed here: Swift toolchain is not installed.
protocol Reportable {
func headline() -> String
}
extension Reportable {
func headline() -> String { "Untitled" }
func category() -> String { "general" }
}
struct Incident: Reportable {
func headline() -> String { "Disk full" }
func category() -> String { "operations" }
}
let incident = Incident()
let report: any Reportable = incident
print(incident.headline())
print(report.headline())
print(incident.category())
print(report.category())Disk full
Disk full
operations
generalheadline() selects the Incident implementation through conformance, so concrete and existential calls agree. category() has no corresponding requirement witness; when the variable’s static type is any Reportable, the compiler selects the protocol-extension implementation.
If category is meant to be replaceable behavior, add func category() -> String to Reportable and keep the default in the extension. The concrete implementation then applies to every call through the protocol boundary, not only calls made directly through the concrete type.
Preserving or erasing an associated type
Feed names Item as its primary associated type. The factory uses some Feed<String> to hide its concrete implementation while preserving the element type. The array uses any Feed<String> to hold different conformers while limiting its interface to protocol-guaranteed operations.
// # not executed here: Swift toolchain is not installed.
protocol Feed<Item> {
associatedtype Item
func next() -> Item?
}
struct Single<Value>: Feed {
let value: Value
func next() -> Value? { value }
}
struct Empty<Value>: Feed {
func next() -> Value? { nil }
}
func releaseFeed() -> some Feed<String> {
Single(value: "release")
}
let feeds: [any Feed<String>] = [
releaseFeed(),
Empty<String>()
]
for feed in feeds {
print(feed.next() ?? "none")
}release
noneA function returning some Feed<String> must choose the same underlying type on every return path. The caller can’t name that type, but it knows Item == String; this preserves more information than an unconstrained protocol boundary.
The array needs to hold both Single<String> and Empty<String>, so it uses any Feed<String>. If every element already has one concrete type, a generic collection preserves more static relationships and is usually easier to feed into further generic algorithms.
Composing the capabilities needed locally
When a function needs a name and a priority, it can spell a protocol composition directly instead of inventing a protocol for a one-off intersection. An existential array can hold several concrete types that satisfy both contracts.
// # not executed here: Swift toolchain is not installed.
protocol Named {
var name: String { get }
}
protocol Prioritized {
var priority: Int { get }
}
struct BuildJob: Named, Prioritized {
let name: String
let priority: Int
}
struct SupportTicket: Named, Prioritized {
let name: String
let priority: Int
}
let queue: [any Named & Prioritized] = [
BuildJob(name: "compile", priority: 2),
SupportTicket(name: "login", priority: 1)
]
for item in queue.sorted(by: { $0.priority < $1.priority }) {
print("\(item.priority):\(item.name)")
}1:login
2:compileNamed & Prioritized describes only the capability intersection at this storage location; it doesn’t create a new name that other protocols can inherit. If the queue contract later gains cancellation, retry, or concurrency-safety requirements, declare a named protocol and evolve it in one place.
This array chooses any because the two elements have different concrete types. If a function processes one caller-provided value at a time, a generic constraint <Item: Named & Prioritized> preserves that call’s concrete type.
Pitfalls
Treating extension members as overridable requirements
Fix: members that need polymorphic replacement must appear in the protocol declaration first. Let the extension provide only the default witness, and test calls through both the concrete and existential types.
Replacing every generic with any
Fix: write down the same-type relationships callers need to retain. Choose any only when a storage slot must change concrete type at runtime or a collection truly needs heterogeneous elements; otherwise consider a generic or some boundary.
Misreading { get } and { get set }
Fix: define the smallest access capability the consumer needs. Don’t widen the protocol because one current implementation happens to use var; require a setter only when protocol consumers must assign through it.
Assuming an existential conforms to its protocol
Fix: identify the Self and associated-type relationships required by the call. Let the compiler open an existential for a direct call when possible; when relationships must persist across values, use a generic parameter, constrain a primary associated type, or design an explicit type-erasing wrapper.
Letting a default hide an omission
Fix: use defaults only for semantics that are correct for every conformer. Require explicit implementations for security- or business-critical behavior, or test that each conformer deliberately accepts the default.
Conformance witnesses and call selection
When the compiler accepts a conformance, it maps each requirement to a concrete implementation, commonly called a witness. A call through a protocol boundary can use conformance information to find the right implementation. The language guarantees observable dispatch semantics; an application shouldn’t treat one compiler version’s table layout as ABI.
A default implementation of a requirement from a protocol extension can also be a witness. If the concrete type provides its own matching implementation when conformance is declared, that implementation is selected. The important boundary is still whether the member appears in the protocol declaration, not whether its implementation happens to live in the type body or another extension file.
An extension-only member has no witness slot. The compiler resolves it using the expression’s static type: a same-named concrete member can win when the static type is concrete, while the extension member is visible when the static interface exposes only the protocol. Saying “protocol methods use dynamic dispatch” without this qualification creates the wrong prediction.
A generic function still carries conformance information, and the optimizer may specialize or inline code for a concrete type, but source semantics don’t promise a particular machine-code layout. Measure performance in a release build on the target platform. Choosing a generic or existential boundary is first a decision about API type relationships and runtime flexibility.
Existentials and associated types
An existential packages a current concrete value with its conformance information inside an abstract container. The variable can later accept another conforming type, so concrete type identity isn’t part of the static interface. Callers can rely only on protocol requirements and primary-associated-type constraints retained explicitly in the signature.
Older material often says that “a protocol with associated types can’t be used as a type.” Modern Swift supports any P and can implicitly open an existential in many calls. The actual limit is that erasure may leave too little information to establish a required relationship. If a method accepts Item, for example, an unconstrained any Feed doesn’t tell the caller which concrete Item it can pass.
any Feed<String> retains Item == String, so operations that read String? have a known type. If two separate existentials must share the same unknown Item, boxing them independently still doesn’t prove that relationship. Lifting their common type into a generic parameter lets the compiler check the invariant across values.
A handwritten type-erasing wrapper can store required operations in closures and retain an associated type in the wrapper’s generic parameter. It is useful when an API needs a stable named type, extra state, or compatibility with an older boundary, but it adds forwarding code. Don’t generate an AnyP wrapper mechanically when modern any already satisfies the requirement.
The fixed identity of opaque types
Return-position some P hides an underlying type name, but each declaration still chooses one fixed underlying type. A function can’t return unrelated A and B types from separate branches merely because both conform to P. Return any P or unify the choices with an enumeration when runtime switching is required.
An opaque type retains underlying type identity, so the compiler can preserve associated-type and Self relationships. A caller can’t spell the hidden name, but it can rely on constraints exposed by the declaration. some Collection<String> hides the concrete collection while publishing Element == String.
Parameter-position some P is shorthand for a generic parameter, so each call chooses the concrete type. Return-position some P gives that choice to the implementation. The same keyword reverses ownership in the two positions, and a generated signature review must make that explicit.
Conditional conformance and composition boundaries
A generic type can conform to a protocol only when its type parameter meets a condition; this is conditional conformance . If a wrapper can compare contents only when Value: Equatable, put that condition on the conformance extension instead of promising an ability that some instances can’t provide.
// # not executed here: Swift toolchain is not installed.
struct Box<Value> {
let value: Value
}
extension Box: Equatable where Value: Equatable {}
print(Box(value: 3) == Box(value: 3))trueThis declaration gives Box<Int> Equatable conformance without pretending that Box<NonEquatable> is comparable. The condition belongs to the conformance itself, so consumers can keep using generic constraints and let the compiler prove whether the capability exists.
Adding conformance between a type and protocol that both come from other modules is retroactive conformance. Another module can declare the same pairing, creating a conflict when both reach one program. Prefer a local wrapper that owns the conformance; when retroactive conformance is necessary, treat global uniqueness as an integration contract rather than merely making the current file compile.
Don’t confuse protocol inheritance with protocol composition either. protocol CachedStore: Store, Sendable declares a named long-term contract that other protocols can inherit. A parameter any Store & Sendable asks for the intersection only at that position. Where the contract is owned and evolved determines which form fits.
Testing a protocol boundary
A protocol test shouldn’t verify only one concrete implementation. Prepare at least two conformers with different behavior to expose misuse of defaults, concrete-type leakage, and disagreement between a test double and production implementation. If there is only one implementation, first confirm that the protocol creates a valuable replacement boundary.
A dispatch test should call the same value first through its concrete type and then through any P. If the paths disagree, check whether a member was omitted from the requirements. Associated-type tests should cover both allowed same-type combinations and different-type combinations that compilation is supposed to reject.
An existential collection also needs a type-switching test. Insert two conforming types and operate on each only through the protocol interface, confirming that code doesn’t rely on a hidden concrete type. If the operation fills with forced casts, the protocol may not express the truly shared capability.
Further reading
4 questions · 1 predict-the-output · 1 spot-the-bug