Protocols and Extensions
Protocols define contracts for types, enabling polymorphism, while extensions add functionality to existing types. Together, they form the backbone of Swift’s protocol-oriented programming.
Protocols
Protocols specify requirements (properties, methods, initializers) that conforming types must implement.
Defining a Protocol
Example:
protocol Vehicle {
var speed: Double { get }
var id: String { get set }
func accelerate(by: Double)
init(id: String)
}
struct Bicycle: Vehicle {
var speed: Double = 0.0
var id: String
func accelerate(by: Double) {
speed += max(0, by)
}
init(id: String) {
self.id = id
}
}
Protocol Inheritance
Protocols can inherit multiple protocols.
Example:
protocol Electric {
var batteryLevel: Double { get }
}
protocol ElectricVehicle: Vehicle, Electric {
func charge()
}
class Tesla: ElectricVehicle {
var speed: Double = 0.0
var id: String
var batteryLevel: Double = 100.0
func accelerate(by: Double) {
speed += by
batteryLevel -= by / 10
}
func charge() {
batteryLevel = 100.0
}
init(id: String) {
self.id = id
}
}
Associated Types
Allow protocols to work with generic types.
Example:
protocol Container {
associatedtype Item
var items: [Item] { get }
mutating func add(_ item: Item)
}
struct Stack<T>: Container {
var items: [T] = []
mutating func add(_ item: T) {
items.append(item)
}
}
Protocol Composition
Combine multiple protocols using &
.
Example:
protocol Named {
var name: String { get }
}
func describe(_ item: Named & Vehicle) {
print("\(item.name) with speed \(item.speed)")
}
Optional Requirements
Use @objc
and optional
for optional protocol members (Objective-C interop).
Example:
@objc protocol Delegate {
@objc optional func didFinish()
}
Extensions
Extensions add methods, properties, initializers, or protocol conformances to types.
Basic Extension
Example:
extension Int {
var squared: Int {
return self * self
}
func isEven() -> Bool {
return self % 2 == 0
}
}
print(5.squared) // 25
print(4.isEven()) // true
Protocol Conformance via Extension
Add protocol conformance to existing types.
Example:
protocol Describable {
var description: String { get }
}
extension String: Describable {
var description: String {
return "\"\(self)\""
}
}
print("Hello".description) // "Hello"
Extension with Constraints
Limit extensions to specific conditions.
Example:
extension Collection where Element: Equatable {
func countOccurrences(of item: Element) -> Int {
return self.filter { $0 == item }.count
}
}
let array = [1, 2, 2, 3]
print(array.countOccurrences(of: 2)) // 2
Protocol Extensions
Provide default implementations for protocol requirements.
Example:
protocol Loggable {
func log()
}
extension Loggable {
func log() {
print("Logging \(Self.self)")
}
}
struct User: Loggable {}
User().log() // "Logging User"
Conditional Protocol Extensions
Apply defaults based on constraints.
Example:
extension Loggable where Self: Describable {
func log() {
print("Logging \(description)")
}
}
struct Item: Loggable, Describable {
var description: String { "Item" }
}
Item().log() // "Logging Item"
Protocol-Oriented Programming (POP)
Swift encourages POP, where protocols and extensions define behavior over inheritance.
Example:
protocol Renderable {
func render()
}
extension Renderable {
func render() {
print("Rendering \(Self.self)")
}
}
struct Shape: Renderable {}
struct Text: Renderable {}
let renderables: [Renderable] = [Shape(), Text()]
renderables.forEach { $0.render() }
// Output: Rendering Shape
// Rendering Text
Best Practices
- Protocol Granularity: Define small, focused protocols.
- Default Implementations: Use extensions to reduce boilerplate.
- Associated Types: Use for flexible, generic protocols.
- Composition: Prefer protocol composition over deep inheritance.
- Avoid Optional Requirements: Use Swift-native protocols when possible.
- Extension Organization: Group related extensions in separate files.
- POP over OOP: Favor protocols for flexibility and testability.
Troubleshooting
- Protocol Conformance Errors: Ensure all requirements are implemented.
- Associated Type Ambiguity: Use type annotations or constraints.
- Extension Conflicts: Resolve with more specific constraints.
- Optional Protocol Issues: Verify
@objc
for Objective-C compatibility. - Performance: Avoid excessive protocol dispatching in critical paths.
Example: Comprehensive Protocol and Extension Usage
protocol Identifiable {
associatedtype ID: Hashable
var id: ID { get }
}
protocol Storable {
func save()
}
protocol Manageable: Identifiable, Storable {
func delete()
}
extension Manageable {
func delete() {
print("Deleting item with ID: id\(id)")
}
}
struct Product: Manageable {
typealias ID = String
let id: String
let name: String
func save() {
print("Saving product \(name) with id: id\(id)")
}
}
extension Collection where Element: Manageable {
func saveAll() {
forEach { $0 }.save()
}
func deleteAll() {
forEach { $0 }.delete()
}
}
let products = [
Product(id: "P1", name: "Laptop"),
Product(id: "P2", name: "Phone")
]
products.saveAll()
// Output:
// Saving product Laptop with id: idP1
// Saving product Phone with id: idP2
products.deleteAll()
// Output:
// Deleting item with id: idP1
// Deleting item with id: idP2