Protocols describe capabilities that types can adopt. Protocol extensions let you provide shared behavior for those types without requiring a common superclass.
Defining a Protocol
This protocol describes anything that can produce a display title:
protocol Displayable {
var name: String { get }
var subtitle: String? { get }
}
Every conforming type must provide the required properties.
Adding Default Behavior
A protocol extension can provide a reusable implementation:
extension Displayable {
var subtitle: String? {
nil
}
var displayTitle: String {
if let subtitle {
return "\(name) - \(subtitle)"
}
return name
}
}
Now a type only needs to implement name when it does not need a subtitle:
struct Category: Displayable {
let name: String
}
struct Product: Displayable {
let name: String
let subtitle: String?
}
Constrained Extensions
Extensions can be available only when the conforming type meets another requirement:
protocol Resettable {
mutating func reset()
}
extension Collection where Element: Resettable {
mutating func resetAll() {
for index in indices {
self[index].reset()
}
}
}
Constraints make APIs discoverable while preventing methods from appearing on incompatible types.
Composing Protocols
Use protocol composition when a value must support multiple capabilities:
protocol Named {
var name: String { get }
}
protocol Persistable {
func save() throws
}
func saveNamedItem(_ item: Named & Persistable) throws {
print("Saving \(item.name)")
try item.save()
}
Protocol Extension or Helper Type?
Use a protocol extension when the behavior naturally belongs to every conforming type. Prefer a separate helper type when the operation needs its own state or has a different responsibility.
Static Dispatch and Surprises
Protocol extensions are very useful, but there is one detail that can surprise you: not every method in a protocol extension behaves like a normal protocol requirement. If a method is declared only in the extension, Swift can use static dispatch for that method. If the method is declared in the protocol itself, conforming types can provide their own implementation and calls through an existential will use the conforming type's version.
protocol Greeter {
func greet()
}
extension Greeter {
func greet() {
print("Hello from the protocol")
}
func farewell() {
print("Goodbye from the protocol extension")
}
}
struct FriendlyGreeter: Greeter {
func greet() {
print("Hello from FriendlyGreeter")
}
func farewell() {
print("Goodbye from FriendlyGreeter")
}
}
let greeter: Greeter = FriendlyGreeter()
greeter.greet()
greeter.farewell()
greet() is a protocol requirement, so FriendlyGreeter can replace the default behavior. farewell() is not a requirement, so the call through Greeter uses the extension implementation. This is not a bug, but it is a good reason to put every behavior you expect conforming types to customize into the protocol declaration.
Designing Small Protocols
Small protocols are easier to adopt than large protocols. If a protocol has ten requirements, every conforming type needs to understand all ten, even if it only cares about one or two. A better approach is to split capabilities into focused protocols and then compose them when a function needs more than one capability.
protocol Titled {
var title: String { get }
}
protocol Archivable {
mutating func archive()
}
typealias ArchivableItem = Titled & Archivable
This also makes defaults more useful. You can add a default displayTitle, a default sort order, or a helper method to a narrow protocol without forcing unrelated types to adopt behavior they do not need.
Testing Default Implementations
Because protocol extension logic is shared across many types, i like to test it with a tiny test type. That keeps the test focused on the default behavior instead of a real app model with extra details.
struct TestDisplayable: Displayable {
let name: String
let subtitle: String?
}
let item = TestDisplayable(name: "Settings", subtitle: "General")
print(item.displayTitle)
If the default behavior is important, give it the same level of testing as any other reusable code. A broken default implementation can quietly affect every type that relies on it.
Wrap up
Protocol extensions are useful for sharing focused behavior across structs, enums, and classes. Keep protocols small, provide sensible defaults, and use constraints to expose behavior only where it makes sense.
Resources: