Defining Protocols
Defining Protocols
A protocol defines a blueprint of requirements—properties, methods, and other constraints—that a type can adopt. Protocols are central to protocol-oriented programming (POP), which is Swift's preferred approach to polymorphism.
Basic Protocol
protocol Describable {
var description: String { get }
func describe() -> String
}
Adopting a Protocol
Any type (struct, class, enum) can adopt a protocol by implementing its requirements:
struct Person: Describable {
var name: String
var age: Int
var description: String {
return "\(name), age \(age)"
}
func describe() -> String {
return "Person: \(description)"
}
}
Protocol Properties
Protocols can require properties with specific access levels and whether they are gettable or gettable/settable:
protocol Identifiable {
var id: String { get } // read-only
}
protocol Mutable {
var value: String { get set } // read-write
}
struct User: Identifiable, Mutable {
let id: String // read-only satisfies { get }
var value: String // read-write satisfies { get set }
}
Protocol Methods
protocol Greetable {
func greet() -> String
mutating func reset() // for value types
}
struct Greeter: Greetable {
var count = 0
func greet() -> String {
return "Hello!"
}
mutating func reset() {
count = 0
}
}
The mutating keyword is needed when the method modifies the type (for structs and enums).
Protocol Initializers
protocol Decodable {
init(from string: String)
}
struct User: Decodable {
var name: String
init(from string: String) {
name = string
}
}
When a class conforms, you must use required init or convenience init:
class User: Decodable {
var name: String
required init(from string: String) {
name = string
}
}
Multiple Protocol Conformance
protocol Printable {
func printInfo()
}
protocol Loggable {
func log()
}
struct Event: Printable, Loggable {
func printInfo() { print("Event") }
func log() { print("Logged event") }
}
Protocol Inheritance
Protocols can inherit from other protocols:
protocol Named {
var name: String { get }
}
protocol Aged: Named {
var age: Int { get }
}
struct Person: Aged {
var name: String
var age: Int
}
A type conforming to Aged must satisfy both Aged and Named requirements.
Protocol Extensions
Protocol Extensions
Protocol extensions let you provide default implementations for protocol requirements. This is the foundation of protocol-oriented programming in Swift.
Default Implementations
protocol Greetable {
func greet() -> String
}
extension Greetable {
func greet() -> String {
return "Hello!"
}
}
struct Person: Greetable {
var name: String
// greet() is provided by the extension
}
let p = Person(name: "Alice")
p.greet() // "Hello!" — from extension
Adding Functionality to Protocols
Extensions can add new methods, computed properties, and subscripts that are not part of the original protocol:
protocol Collection {
var count: Int { get }
subscript(index: Int) -> String { get }
}
extension Collection {
func summary() -> String {
return "Collection with \(count) items"
}
func firstItem() -> String? {
guard count > 0 else { return nil }
return self[0]
}
}
struct StringArray: Collection {
var items: [String]
var count: Int { items.count }
subscript(index: Int) -> String { items[index] }
}
let arr = StringArray(items: ["a", "b", "c"])
arr.summary() // "Collection with 3 items"
arr.firstItem() // Optional("a")
Protocol Inheritance with Extensions
protocol Stackable {
associatedtype Element
mutating func push(_ element: Element)
mutating func pop() -> Element?
}
extension Stackable {
var isEmpty: Bool {
// This is a simplified example
// Real implementation would check internal state
return false
}
}
Specializing Protocol Extensions
You can specialize extensions for types that conform to additional protocols:
protocol Describable {
var description: String { get }
}
extension Describable {
var description: String { return "Default" }
}
// Specialized extension for Equatable types
extension Describable where Self: Equatable {
var description: String {
return "Equatable: \(self)"
}
}
Protocol Extensions vs Protocol Requirements
An important subtlety: methods defined in the protocol itself are dispatched dynamically (through the protocol witness table), while methods defined only in the extension are dispatched statically:
protocol Animal {
func sound() -> String // protocol requirement
}
extension Animal {
func sound() -> String { return "..." } // default impl
func breathe() -> String { return "breathing" } // extension only
}
struct Dog: Animal {
func sound() -> String { return "Woof" }
}
let animal: Animal = Dog()
animal.sound() // "Woof" — dynamic dispatch
animal.breathe() // "breathing" — static dispatch from extension
This distinction matters when overriding default implementations in subclasses.
Protocol Composition
Protocol Composition
Protocol composition lets you combine multiple protocol requirements into a single constraint using the & operator.
Basic Composition
func printInfo(_ item: Identifiable & Describable) {
print("ID: \(item.id), Desc: \(item.description)")
}
struct User: Identifiable, Describable {
let id: String
var description: String { "User \(id)" }
}
printInfo(User(id: "123")) // "ID: 123, Desc: User 123"
Type Aliases for Composition
When composing many protocols, create a type alias for clarity:
typealias Presentable = Identifiable & Describable & Printable
func display(_ item: Presentable) {
print(item.description)
item.printInfo()
}
Composition with Classes
You can compose a class with protocols:
func configure(_ view: UIView & Configurable & Animatable) {
view.configure()
view.animate()
}
Where Clauses
You can constrain associated types using where clauses:
protocol Container {
associatedtype Item
var count: Int { get }
mutating func push(_ item: Item)
}
extension Container where Item: Equatable {
func contains(_ item: Item) -> Bool {
// simplified
return false
}
}
Protocol Existential Types
When you use a protocol as a type, you create an existential type:
let items: [Describable] = [
Person(name: "Alice"),
Product(name: "Phone")
]
for item in items {
print(item.description)
}
In Swift 5.7+, use any to clarify existential types:
let items: [any Describable] = [...]
Opaque Return Types
some Protocol hides the concrete type while guaranteeing it conforms to the protocol:
func makeCounter() -> some Countable {
return Counter()
}
This preserves type information while abstracting implementation details.
Real-World Example
protocol Cacheable {
associatedtype Key: Hashable
associatedtype Value
func get(_ key: Key) -> Value?
mutating func set(_ key: Key, value: Value)
}
struct MemoryCache<K: Hashable, V>: Cacheable {
typealias Key = K
typealias Value = V
private var storage: [K: V] = [:]
func get(_ key: K) -> V? { storage[key] }
mutating func set(_ key: K, value: V) { storage[key] = value }
}
extension Cacheable where V: Codable {
func persist(to url: URL) throws {
// save to disk
}
}
Protocol composition and extensions enable flexible, testable architectures. By depending on protocols rather than concrete types, you achieve loose coupling and easier unit testing.
Quiz
1. What does a protocol define?
2. What does `&` do in a type constraint?
3. What is the difference between a protocol requirement and a protocol extension method?
4. What does `some Protocol` return?
5. Can structs adopt protocols?
Flashcards
Question
What is a protocol in Swift?
Click to reveal answer
Answer
A blueprint defining requirements (properties, methods) that types can adopt to conform.
Question
What does `mutating` mean in a protocol method?
Click to reveal answer
Answer
The method can modify the adopting type's properties (needed for structs and enums).
Question
What is protocol composition?
Click to reveal answer
Answer
Combining multiple protocols with `&` to create a single type constraint.
Question
What is an existential type?
Click to reveal answer
Answer
Using a protocol as a type (`let x: Protocol` or `any Protocol`), which can hold any conforming instance.
Question
What is the advantage of protocol-oriented programming?
Click to reveal answer
Answer
Loose coupling, testability, value type support, and avoiding inheritance hierarchies.
Revision Notes
Key Takeaways
- 1. Protocols define blueprints; types adopt them with conformance
- 2. Protocol extensions provide default implementations
- 3. Composition (`&`) combines protocol constraints
- 4. Protocol requirements use dynamic dispatch; extensions use static
- 5. Prefer protocols over class inheritance for flexibility
Interview Tips
- • Explain protocol-oriented programming vs object-oriented programming
- • Know the difference between existential and opaque types
- • Be able to create protocol extensions with default implementations
- • Understand when to use `where` clauses on protocol extensions
Cheat Sheet
Protocols & Extensions Cheat Sheet
Defining:
protocol Name { }— defines requirements- Properties:
{ get }or{ get set } - Methods:
func method()ormutating func method()
Adopting:
struct Type: Protocol1, Protocol2 { }- Must implement all protocol requirements
Extensions:
- Provide default implementations:
extension Protocol { } - Add new methods not in the protocol
- Specialize with
whereclauses
Composition:
A & B— combines two protocol constraints- Type alias:
typealias Combined = A & B
Existentials:
let x: Protocolorlet x: any Protocolsome Protocol— opaque return type