Chapters

Hide chapters

Swift Apprentice

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Advanced Topics

Section 4: 10 chapters
Show chapters Hide chapters

16. 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.

In this book, you’ve learned about the three named types: structs, classes and enums. There’s one more named type to learn about: the protocol.

Unlike the other named types, protocols don’t define anything you instantiate directly. Instead, they define an interface or blueprint that actual concrete types conform to. With a protocol, you define a common set of properties and behaviors that concrete types go and implement.

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

Introducing protocols

You define a protocol much as you do any other named type. Enter the following into a playground:

protocol Vehicle {
  func accelerate()
  func stop()
}

The keyword protocol is followed by the name of the protocol, followed by the curly braces with the members of the protocol inside. The big difference you’ll notice is that the protocol doesn’t contain any implementation.

That means you can’t instantiate a Vehicle directly:

Instead, you use protocols to enforce methods and properties on other types. What you’ve defined here is something like the idea of a vehicle — it’s something that can accelerate and stop.

Protocol syntax

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 {
  var peddling = false

  func accelerate() {
    peddling = true
  }

  func stop() {
    peddling = false
  }
}

Methods in protocols

In the Vehicle protocol above, you define a pair of methods, accelerate() and stop(), that all types conforming to Vehicle must implement.

enum Direction {
  case left
  case right
}

protocol DirectionalVehicle {
  func accelerate()
  func stop()
  func turn(_ direction: Direction)
  func description() -> String
}
protocol OptionalDirectionVehicle {
  // Build error!
  func turn(_ direction: Direction = .left)
}
protocol OptionalDirectionVehicle {
  func turn()
  func turn(_ direction: Direction)
}

Properties in protocols

You can also define properties in a protocol:

protocol VehicleProperties {
  var weight: Int { get }
  var name: String { get set }
}

Initializers in protocols

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

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
  }
}

var accountType: Account.Type = BitcoinAccount.self
let account = accountType.init(initialAmount: 30.00)
let transferAccount = accountType.init(transferAccount: account)!

Protocol inheritance

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

protocol WheeledVehicle: Vehicle {
  var numberOfWheels: Int { get }
  var wheelSize: Double { get set }
}

Mini-exercises

  1. Create a protocol Area 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. Convert the array of shapes to an array of areas using map.

Implementing protocols

As you’ve already seen, when you declare your type as conforming to a protocol, you must implement all the requirements declared in the protocol:

class Bike: Vehicle {
  var peddling = false
  var brakesApplied = false

  func accelerate() {
    peddling = true
    brakesApplied = false
  }

  func stop() {
    peddling = false
    brakesApplied = true
  }
}

Implementing properties

Recall that properties in protocols come with a get and possibly a set requirement and that a conforming type must conform to at least these requirements.

class Bike: WheeledVehicle {

  let numberOfWheels = 2
  var wheelSize = 16.0

  var peddling = false
  var brakesApplied = false

  func accelerate() {
    peddling = true
    brakesApplied = false
  }

  func stop() {
    peddling = false
    brakesApplied = true
  }
}

Associated types in protocols

You can also add an associated type as a protocol member. When using associatedtype in a protocol, you’re simply stating there is a type used in this protocol, without specifying what type this should be. It’s up to the protocol adopter to decide what the exact type should be.

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

  var weight: Int { 100 }
}

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

  var weight: Double { 0.0025 }
}
// Build error!
// protocol 'WeightCalculatable' can only be used as a generic
// constraint because it has Self or associated type requirements.
let weightedThing: WeightCalculatable = LightThing()

Implementing multiple protocols

A class can only inherit from a single class — this is the property of “single inheritance”. In contrast, a class (struct or enum) can be made to conform to as many protocols as you’d like!

protocol Wheeled {
  var numberOfWheels: Int { get }
  var wheelSize: Double { get set }
}

class Bike: Vehicle, Wheeled {
  // Implement both Vehicle and Wheeled
}

Protocol composition

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 stop() function and the Wheeled protocol’s numberOfWheels property. You can do this using the & composition operator.

func roundAndRound(transportation: Vehicle & Wheeled) {
    transportation.stop()
    print("The brakes are being applied to
          \(transportation.numberOfWheels) wheels.")
}

roundAndRound(transportation: Bike())
// The brakes are being applied to 2 wheels.

Extensions & protocol conformance

You can also adopt protocols using extensions. This lets you add protocol conformances to types you don’t necessarily own. Consider the simple example below which adds a custom protocol to String:

protocol Reflective {
  var typeName: String { get }
}

extension String: Reflective {
  var typeName: String {
    "I’m a String"
  }
}

let title = "Swift Apprentice!"
title.typeName // I’m a String
class AnotherBike: Wheeled {
  var peddling = false
  let numberOfWheels = 2
  var wheelSize = 16.0
}

extension AnotherBike: Vehicle {
  func accelerate() {
    peddling = true
  }
  
  func stop() {
    peddling = false
  }
}

Requiring reference semantics

Protocols can be adopted by both value types (structs and enums) and reference types (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: class {
  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) be able to let you specify every conceivable requirement that the compiler can check. For example, a protocol may need to specify complexity requirements (O(1) vs O(n)) for an operation. It can do this only by stating it in comments. It is important for you to understand all of the requirements that a protocol makes to correctly conform.

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

While learning the entire Swift standard library isn’t vital to your success as a Swift developer, there are a few other important protocols you’ll find useful 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"]

CustomStringConvertible

The very handy 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 if you try to solve it yourself, but, as always, a solution is available if you get stuck.

Pet shop tasks

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

Key points

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.
© 2023 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