Skip to content

Protocols & Extensions

Protocols and Extensions are core to Swift’s "Protocol-Oriented Programming" paradigm.

Protocols

A Protocol defines a blueprint of methods, properties, and other requirements. It doesn't provide the code itself, but it ensures that any type that adopts it will have certain features.

swift
protocol Device {
    // Property requirement ({ get set } means it must be readable and writable)
    var id: String { get set }
    
    // Initializer requirement
    init(id: String)
    
    // Standard method requirement
    func turnOn()
    
    // Mutating method requirement (needed if the method changes the instance itself)
    mutating func reset()
}

struct Phone: Device {
    var id: String
    
    // Conforming to the initializer requirement
    init(id: String) {
        self.id = id
    }
    
    // Conforming to the method requirements
    func turnOn() {
        print("Phone \(id) is booting up.")
    }
    
    mutating func reset() {
        id = "Default"
    }
}

Multiple Protocols

A single type can adopt as many protocols as it needs. You simply list them after the colon and separate them with commas.

swift
struct SmartWatch: Device, Equatable, CustomStringConvertible {
    // Implementation goes here
}

Why use Protocols?

Adopting a protocol allows you to write generic code that works with any type as long as it conforms to the blueprint. For example, a function could accept a list of "Device" items and call turnOn() on all of them, regardless of whether they are Phones, Tablets, or Laptops.

Using Protocols as Types (some vs any)

When using a protocol as a parameter or a return type, Swift asks you to clarify how that protocol should be handled using the some or any keywords.

The some Keyword (Opaque Types)

Using some tells the compiler that the underlying type is completely fixed and will never change, even though the exact type name is hidden from the caller. It is highly efficient and is the standard way to return UI components in SwiftUI.

swift
// The caller just knows it gets a Device back.
// The compiler guarantees it will ALWAYS be a Phone.
func createDevice() -> some Device {
    return Phone(id: "123") 
}

The any Keyword (Existential Types)

Using any creates a flexible box that can hold literally any type conforming to the protocol. The underlying type inside that box can change while the app is running. This requires a bit more memory but is necessary when you need to mix entirely different types together.

swift
// This array can hold a mix of Phones, Tablets, and Laptops
let myDevices: [any Device] = [Phone(id: "1"), Tablet(id: "2")]

Extensions

Extensions add new functionality to an existing class, struct, enum, or protocol type. This includes types for which you do not have the original source code (like built-in types).

swift
extension Int {
    func squared() -> Int {
        self * self
    }
}

print(7.squared()) // 49

Protocol Extensions

You can extend a protocol to provide a default implementation to all conforming types. This is incredibly powerful as it adds functionality to multiple types at once.

swift
extension Device {
    func displayID() {
        print("Device ID is: \(id)")
    }
}

// Usage Example:
var myPhone = Phone(id: "PHN-01")
myPhone.displayID() // "Device ID is: PHN-01" -> Phone got this method for free!

Extensions vs Inheritance

While both allow you to add functionality, they serve different purposes:

FeatureInheritance (Classes Only)Extensions (All Types)
Relationship"Is-a": Creates a child subclass."Skills": Adds a new ability to existing type.
StorageCan add new Stored Properties.Cannot add Stored Properties.
OverrideCan change parent behavior via override.Can only add new behavior.

NOTE

Extensions are the preferred way to grow your code in Swift. They keep things simple by adding "skills" without creating a messy "family tree" (inheritance).

Built-in Protocols

Swift has many powerful built-in protocols that you can adopt to make your custom types feel like native ones. Some of them are:

Equatable

Allows you to compare two instances using ==.

swift
struct Point: Equatable {
    var x: Int, y: Int
}

let a = Point(x: 1, y: 2)
let b = Point(x: 1, y: 2)
print(a == b) // true

Comparable

Allows you to use operators like <, >, and sorting.

swift
struct Score: Comparable {
    var value: Int
    static func < (lhs: Score, rhs: Score) -> Bool {
        lhs.value < rhs.value
    }
}

let high = Score(value: 100)
let low = Score(value: 50)
print(low < high) // true

Hashable

Allows your type to be used as a key in a Dictionary or stored securely in a Set. Swift can automatically generate the background code for this as long as all the properties inside your type are also Hashable.

swift
struct User: Hashable {
    var username: String
}

Codable

Allows your type to be easily converted to and from external data formats like JSON. Like Hashable, Swift almost always generates the required code for you automatically.

swift
struct Player: Codable {
    var name: String
    var score: Int
}

Identifiable

Requires your type to have a unique id property so that Swift can tell individual instances apart. This protocol is essential when building user interfaces with SwiftUI, as it allows lists to track exactly which items are added, removed, or changed.

swift
import Foundation

struct Book: Identifiable {
    let id = UUID() // Automatically generates a unique identifier
    var title: String
}

let myBook = Book(title: "Treasure Island")
print(myBook.id) // e.g., 550E8400-E29B-41D4-A716-446655440000

CustomStringConvertible

Allows you to customize how your type is printed in print() or converted to a String.

swift
struct Pirate: CustomStringConvertible {
    var name: String
    var description: String { "Ahoy! I am \(name)." }
}

let p = Pirate(name: "Jack")
print(p) // "Ahoy! I am Jack."