Chapters

Hide chapters

Swift Apprentice: Fundamentals

First Edition · iOS 16 · Swift 5.7 · Xcode 14.2

Section III: Building Your Own Types

Section 3: 9 chapters
Show chapters Hide chapters

17. Protocols
Written by Ehab Amer

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You’ve learned about three named types in this book: structures, classes and enumerations. There is another very special one: the protocol.

Unlike the other named types, protocols don’t define anything you instantiate directly. Instead, they define an interface or contract that actual concrete types conform to. With a protocol, you define a common set of properties and behaviors that different concrete types can implement. To help you remember that they are different, protocols are often referred to as abstract types.

You’ve been using protocols behind the scenes since the beginning of this book. In this chapter, you’ll learn the details about protocols and see why they’re central to Swift.

Introducing Protocols

You define a protocol much as you do any other named type. Start with this definition for a Vehicle:

protocol Vehicle {
  /// Return a description of the state of the vehicle.
  func describe() -> String

  // more to come...
}

Following the keyword, protocol is its name, followed by curly braces with the requirements inside. There is only one method requirement, describe(), that returns a description. The big difference you’ll notice is that the protocol doesn’t contain any implementation.

That means you can’t instantiate a Vehicle directly:

Delete that line and continue building the Vehicle abstraction. Add the following to the protocol body:

/// Increases speed until it reaches its maximum speed.
mutating func accelerate()

/// Stop moving. Reducing the speed to zero miles per hour.
mutating func stop()

You mark these methods mutating because when they are implemented, they need to change the instance’s state. You can also add a couple of property requirements:

/// The speed of the vehicle in miles per hour.
var speed: Double { get set }

/// The maximum speed attainable by this Vehicle type.
static var maxSpeed: Double { get }

When defining properties in a protocol, you must explicitly mark them as get or get set, similar to how you declare computed properties. However, much like methods, you don’t include any implementation for properties.

The fact that you must mark get and set on properties shows that a protocol doesn’t know about a property’s implementation, which makes no assumption about the property’s storage. You can implement these property requirements as computed properties or as regular variables. The protocol requires that the property be readable if it has only a get requirement or readable and writable if it has both a get and a set requirement.

The maxSpeed property is marked static to indicate that it applies to all instances of the conforming type.

In short, you use protocols to describe the requirements of a type. What you’ve defined here is the abstract idea of a vehicle.

Protocol Adoption

A protocol can be adopted by a class, struct or enum — and when another type adopts a protocol, it’s required to implement the methods and properties defined in the protocol. Once a type implements all members of a protocol, the type is said to conform to the protocol.

class Unicycle: Vehicle {
}

class Unicycle: Vehicle {
  func describe() -> String {
    "Unicycle @ \(speed) mph"
  }  
  func accelerate() {
    speed = min(speed + 2, Self.maxSpeed)
  }
  func stop() {
    speed = 0
  }
  var speed: Double = 0
  static var maxSpeed: Double { 15 }
}
struct Car {
  func describe() -> String {
    "Car @ \(speed) mph"
  }
  mutating func accelerate() {
    speed = min(speed + 20, Self.maxSpeed)
  }
  mutating func stop() {
    speed = 0
  }
  var speed: Double = 0
  static var maxSpeed: Double { 150 }
}
extension Car: Vehicle {}

Defining Implementations

Protocol extensions allow you to define default implementation for protocol definitions. You might notice duplicated code in the example above. You can write a general purpose stop() method like so:

extension Vehicle {
  mutating func stop() {
    speed = 0
  }
}
extension Vehicle {
  /// Return the speed as a value between 0-1.
  var normalizedSpeed: Double {
    speed / Self.maxSpeed
  }
}

Default Parameters

Protocols do not let you define default parameters like you can with functions or methods. But you can work around this limitation. To see how, create the following abstraction for Turnable types:

/// Different pressure options.
enum BrakePressure {
  case light
  case normal
  case hard
}

protocol Braking {
  /// Apply the brakes.
  mutating func brake(_ pressure: BrakePressure = .normal) // ERROR
}
protocol Braking {
  /// Apply the brakes.
  mutating func brake(_ pressure: BrakePressure)
}

extension Braking {
  /// Apply normal brakes.
  mutating func brake() {
    brake(.normal)
  }
}

Initializers in Protocols

While protocols themselves can’t be initialized, they can declare initializers that conforming types must implement:

protocol Account {
  var value: Double { get set }
  init(initialAmount: Double)
  init?(transferAccount: Account)
}
class BitcoinAccount: Account {
  var value: Double
  required init(initialAmount: Double) {
    value = initialAmount
  }
  required init?(transferAccount: Account) {
    guard transferAccount.value > 0.0 else {
      return nil
    }
    value = transferAccount.value
  }
}
let accountType: Account.Type = BitcoinAccount.self
let account = accountType.init(initialAmount: 30)
let transferAccount = accountType.init(transferAccount: account)!

Protocol Inheritance

The Vehicle protocol contains a set of methods that could apply to any vehicle, such as a bike, car, snowmobile, or airplane!

protocol WheeledVehicle: Vehicle {
  var numberOfWheels: Int { get }
  var wheelSize: Double { get }
}
extension Unicycle: WheeledVehicle {
  var numberOfWheels: Int { 1 }
  var wheelSize: Double { 20.0 }
}

Using Protocols

Because each conforming type can supply its own implementation, protocols can be used with any type (structures, enumeration, classes) to achieve polymorphism like traditional base classes. Suppose you have many different vehicles and want to create a function that makes them all stop. You might try to implement it like this:

func stop(vehicles: [Vehicle]) {
  vehicles.forEach { vehicle in
    vehicle.stop() // ERROR: Cannot call a mutating method on a constant array!
  }
}
func stop(vehicles: inout [Vehicle]) {
  vehicles.indices.forEach {
    vehicles[$0].stop()
  }
}
func stop(vehicles: inout [any Vehicle]) {
  vehicles.indices.forEach {
    vehicles[$0].stop()
  }
}

Mini-Exercises

  1. Create an Area protocol that defines a read-only property area of type Double.
  2. Implement Area with structs representing Square, Triangle and Circle.
  3. Add a circle, a square and a triangle to an array. Compute the total area of shapes in the array.

Associated Types in Protocols

Some types are naturally associated together with others. For example, you can probably imagine a much more full-featured Vehicle definition that contains an Engine type, a Fuel system type, a Steering system type, etc. Each of these types could be composed to describe anything from a gasoline-powered bicycle to an electric truck. Swift gives you the power to do this.

protocol WeightCalculatable {
  associatedtype WeightType
  var weight: WeightType { get }
}
struct HeavyThing: WeightCalculatable {
  // This heavy thing only needs integer accuracy
  typealias WeightType = Int

  var weight: Int { 100 }
}

struct LightThing: WeightCalculatable {
  // This light thing needs decimal places
  typealias WeightType = Double

  var weight: Double { 0.0025 }
}

Implementing Multiple Protocols

A class can only inherit from a single class — this is the property of “single inheritance”. By contrast, a class, structure or enumeration can conform to as many protocols as you’d like! Suppose you made Wheeled a protocol instead of the WheeledVehicle protocol earlier. It might look like this:

protocol Wheeled {
  var numberOfWheels: Int { get }
  var wheelSize: Double { get }
}
extension Car: Wheeled {
  var numberOfWheels: Int { 4 }
  var wheelSize: Double { 17 }
}

Protocol Composition and some

In the previous section, you learned how to implement multiple protocols. Sometimes you need a function to take a data type that must conform to multiple protocols. That is where protocol composition comes in. Imagine you need a function that needs access to the Vehicle protocol’s mutable stop() function and the Wheeled protocol’s numberOfWheels property. You can do this using the & composition operator.

func freeze(transportation: inout any Vehicle & Wheeled) {
    transportation.stop()
    print("Stopping the rotation of \(transportation.numberOfWheels) wheel(s).")
}
var car: any Wheeled & Vehicle = Car()
freeze(transportation: &car)
// Stopping the rotation of 4 wheel(s).
func freeze(transportation: inout some Vehicle & Wheeled) {
    transportation.stop()
    print("Stopping the rotation of \(transportation.numberOfWheels) wheel(s).")
}
var car = Car()
freeze(transportation: &car)
// Stopping the rotation of 4 wheel(s).

Requiring Reference Semantics

Protocols can be adopted by both value types (structs and enums) and reference types (such as classes), so you might wonder if protocols have reference or value semantics.

protocol Named {
  var name: String { get set }
}

class ClassyName: Named {
  var name: String
  init(name: String) {
    self.name = name
  }
}

struct StructyName: Named {
  var name: String
}
var named: Named = ClassyName(name: "Classy")
var copy = named

named.name = "Still Classy"
named.name // Still Classy
copy.name  // Still Classy
named = StructyName(name: "Structy")
copy = named

named.name = "Still Structy?"
named.name // Still Structy?
copy.name  // Structy
protocol Named: AnyObject {
  var name: String { get set }
}

Protocols: More Than Bags of Syntax

As you have seen, protocols let you specify many syntax requirements for conforming types. However, they can’t (and never will) let you specify every conceivable requirement for the compiler to check. For example, a protocol may need to specify complexity requirements (O(1) vs. O(n)) for an operation, and it can do this only by stating it in comments. You need to understand all of these requirements that a protocol makes to conform correctly. This reality has led to the refrain that protocols are “more than bags of syntax” that the compiler can check. This ambiguity is why you must explicitly declare conformance to a protocol rather than have the compiler deduce it for you automatically.

Protocols in the Standard Library

The Swift standard library uses protocols extensively in ways that may surprise you. Understanding the roles protocols play in Swift can help you write clean, decoupled “Swifty” code.

Equatable

Some of the simplest code compares two integers with the == operator:

let a = 5
let b = 5

a == b // true
let swiftA = "Swift"
let swiftB = "Swift"

swiftA == swiftB // true
class Record {

  var wins: Int
  var losses: Int

  init(wins: Int, losses: Int) {
      self.wins = wins
      self.losses = losses
  }
}

let recordA = Record(wins: 10, losses: 5)
let recordB = Record(wins: 10, losses: 5)

recordA == recordB // Build error!
protocol Equatable {
  static func ==(lhs: Self, rhs: Self) -> Bool
}
extension Record: Equatable {
  static func ==(lhs: Record, rhs: Record) -> Bool {
    lhs.wins == rhs.wins &&
    lhs.losses == rhs.losses
  }
}
recordA == recordB // true

Comparable

A subprotocol of Equatable is Comparable:

protocol Comparable: Equatable {
  static func <(lhs: Self, rhs: Self) -> Bool
  static func <=(lhs: Self, rhs: Self) -> Bool
  static func >=(lhs: Self, rhs: Self) -> Bool
  static func >(lhs: Self, rhs: Self) -> Bool
}
extension Record: Comparable {
  static func <(lhs: Record, rhs: Record) -> Bool {
    if lhs.wins == rhs.wins {
      return lhs.losses > rhs.losses
    }
    return lhs.wins < rhs.wins
  }
}

“Free” Functions

While == and < are useful in their own right, the Swift library provides you with many “free” functions and methods for types that conform to Equatable and Comparable.

let teamA = Record(wins: 14, losses: 11)
let teamB = Record(wins: 23, losses: 8)
let teamC = Record(wins: 23, losses: 9)
var leagueRecords = [teamA, teamB, teamC]

leagueRecords.sort()
// {wins 14, losses 11}
// {wins 23, losses 9}
// {wins 23, losses 8}
leagueRecords.max() // {wins 23, losses 8}
leagueRecords.min() // {wins 14, losses 11}
leagueRecords.starts(with: [teamA, teamC]) // true
leagueRecords.contains(teamA) // true

Other Useful Protocols

You’ll find a few essential protocols in the Swift standard library that are helpful in almost any project.

Hashable
class Student {
  let email: String
  let firstName: String
  let lastName: String

  init(email: String, firstName: String, lastName: String) {
    self.email = email
    self.firstName = firstName
    self.lastName = lastName
  }
}

extension Student: Hashable {
  static func ==(lhs: Student, rhs: Student) -> Bool {
    lhs.email == rhs.email &&
    lhs.firstName == rhs.firstName &&
    lhs.lastName == rhs.lastName
  }

  func hash(into hasher: inout Hasher) {
    hasher.combine(email)
    hasher.combine(firstName)
    hasher.combine(lastName)
  }
}
let john = Student(email: "johnny.appleseed@apple.com",
                   firstName: "Johnny",
                   lastName: "Appleseed")
let lockerMap = [john: "14B"]
Identifiable
extension Student: Identifiable {
  var id: String {
    email
  }
}

CustomStringConvertible

The convenient CustomStringConvertible protocol helps you log and debug instances.

print(john)
// Student
protocol CustomStringConvertible {
  var description: String { get }
}
extension Student: CustomStringConvertible {
  var description: String {
    "\(firstName) \(lastName)"
  }
}
print(john)
// Johnny Appleseed

Challenge

Before moving on, here is a challenge to test your knowledge of protocols. It is best to try to solve it yourself, but, as always, a solution is available if you get stuck.

Challenge 1: Pet Shop Tasks

Create a collection of protocols for tasks at a pet shop with dogs, cats, fish and birds.

Key Points

  • Protocols define a contract that classes, structs and enums can adopt.
  • Adopting a protocol requires a type to conform to the protocol by implementing all methods and properties of the protocol.
  • You must declare protocol conformance explicitly; it is not enough to implement all protocol requirements.
  • You can use extensions for protocol adoption and conformance.
  • If you create an extension on a protocol that isn’t declared in the protocol, conforming types cannot override the extension.
  • If you create an implementation in an extension declared in the protocol, conforming types can override the extension.
  • A type can adopt any number of protocols, which allows for a quasi-multiple inheritance not permitted through subclassing.
  • any Protocol creates an existential box type to access the underlying type similar to a class base class.
  • some Protocol creates generic access to a concrete type.
  • Protocols are the basis for creating generic code.
  • The Swift standard library uses protocols extensively. You can use many of them, such as Equatable and Hashable, with your own types.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now