Chapters

Hide chapters

Swift Apprentice: Beyond the Basics

First Edition · iOS 16 · Swift 5.8 · Xcode 14.3

Section I: Beyond the Basics

Section 1: 13 chapters
Show chapters Hide chapters

10. Protocol-Oriented Programming
Written by Ehab Amer

Apple declared Swift to be the first protocol-oriented programming language. This declaration was made possible by the introduction of protocol extensions.

Although protocols have been in Swift since the beginning, Apple’s announcement and the protocol-heavy standard library changes affect how you think about your types. Extending protocols is the key to an entirely new style of programming!

In brief, protocol-oriented programming emphasizes coding to protocols instead of specific classes, structs or enums. It does this by breaking the old protocols rules and allowing you to write implementations for protocols on the protocols themselves.

This chapter introduces you to the power of protocol extensions and protocol-oriented programming. Along the way, you’ll learn how to use default implementations, type constraints, mixins and traits to simplify your code vastly.

Introducing Protocol Extensions

You’ve seen extensions in previous chapters. They let you add additional methods and computed properties to a type:

extension String {
  func shout() {
    print(uppercased())
  }
}

"Swift is pretty cool".shout()

Here, you’re extending the String type to add a new method. You can extend any type, including ones you didn’t write yourself and have any number of extensions on a type.

You can define a protocol extension using the following syntax:

protocol TeamRecord {
  var wins: Int { get }
  var losses: Int { get }
  var winningPercentage: Double { get }
}

extension TeamRecord {
  var gamesPlayed: Int {
    wins + losses
  }
}

Just as you extend a class, structure or enumeration, you use the keyword extension followed by the protocol name you are extending. Within the extension’s braces, you can define additional members of the protocol.

Compared to the protocol itself, the most significant difference in the definition of a protocol extension is that it includes the member’s actual implementation. The example above defines a new computed property named gamesPlayed that combines wins and losses and returns the total number of games played.

Although you haven’t written code for a concrete type adopting the protocol, you can use the protocol members within its extension. That’s because the compiler knows that any type conforming to TeamRecord will have all the members required by TeamRecord.

Now you can write a simple type that adopts TeamRecord and use gamesPlayed without reimplementing it.

struct BaseballRecord: TeamRecord {
  var wins: Int
  var losses: Int

  var winningPercentage: Double {
    Double(wins) / Double(wins + losses)
  }
}

let sanFranciscoSwifts = BaseballRecord(wins: 10, losses: 5)
sanFranciscoSwifts.gamesPlayed // 15

Since BaseballRecord conforms to TeamRecord, you can access gamesPlayed, defined in the protocol extension.

You can see how useful protocol extensions can be in defining “free” behavior on a protocol — but this is only the beginning. Next, you’ll learn how protocol extensions can provide implementations for members of the protocol itself.

Default Implementations

A protocol defines a contract for any type that adopts it. If a protocol defines a method or a property, any type that adopts the protocol must implement that method or property. Consider another example of a TeamRecord type:

struct BasketballRecord: TeamRecord {
  var wins: Int
  var losses: Int
  let seasonLength = 82

  var winningPercentage: Double {
    Double(wins) / Double(wins + losses)
  }
}

Both BasketballRecord and BaseballRecord have identical implementations of winningPercentage. You can imagine that most of the TeamRecord types will implement this property similarly. That could lead to a lot of repetitive code.

Fortunately, Swift has a shortcut:

extension TeamRecord {
  var winningPercentage: Double {
    Double(wins) / Double(wins + losses)
  }
}

While this is much like the protocol extension you defined in the previous example, it differs because winningPercentage is declared as a var in the original TeamRecord protocol’s formal definition, whereas gamesPlayed isn’t. Implementing a member of a protocol in an extension creates a default implementation for that member.

You’ve already seen default arguments to functions, and this is similar: If you don’t implement winningPercentage in your type, it’ll use the default implementation provided by the protocol extension.

In other words, you no longer need to explicitly implement winningPercentage on types that adopt TeamRecord:

struct BasketballRecord: TeamRecord {
  var wins: Int
  var losses: Int
  let seasonLength = 82
}

let minneapolisFunctors = BasketballRecord(wins: 60, losses: 22)
minneapolisFunctors.winningPercentage

Default implementations let you add a capability to a protocol while significantly reducing repeated or “boilerplate” code.

A default implementation doesn’t prevent a type from implementing a protocol member on its own. Some team records may require a slightly different formula for the winning percentage, such as a sport that includes ties as a possible outcome:

struct HockeyRecord: TeamRecord {
  var wins: Int
  var losses: Int
  var ties: Int

  // Hockey record introduces ties and has
  // its own implementation of winningPercentage
  var winningPercentage: Double {
    Double(wins) / Double(wins + losses + ties)
  }
}

With this change, if you call winningPercentage on a TeamRecord that’s a HockeyRecord value type, it’ll calculate the winning percentage as a function of wins, losses and ties. If you call winningPercentage on another type that doesn’t have its own implementation, it’ll fall back to the default implementation:

let chicagoOptionals = BasketballRecord(wins: 10, losses: 6)
let phoenixStridables = HockeyRecord(wins: 8, losses: 7, ties: 1)

chicagoOptionals.winningPercentage // 10 / (10 + 6) == 0.625
phoenixStridables.winningPercentage // 8 / (8 + 7 + 1) == 0.5

Mini-Exercise

Write a default implementation on CustomStringConvertible that will remind you to implement description by returning Remember to implement CustomStringConvertible!.

Once you have your default implementation, you can write code like this:

struct MyStruct: CustomStringConvertible {}
print(MyStruct())
// should print "Remember to implement CustomStringConvertible!"

Understanding Protocol Extension Dispatch

There’s a critical pitfall to keep in mind when defining protocol extensions. The interfaces in the formal protocol declaration are customization points that adopting types can override. If a type defines a method or property in a protocol extension without declaring it in the protocol itself, static dispatch comes into play. Static dispatch means the compiler chooses the method or property used at compile-time based on what it knows about the type. The compiler doesn’t account for dynamic runtime information.

Suppose you defined a protocol similar to TeamRecord called WinLoss:

protocol WinLoss {
  var wins: Int { get }
  var losses: Int { get }
}

…and declared the following extension:

extension WinLoss {
  var winningPercentage: Double {
    Double(wins) / Double(wins + losses)
  }
}

…which the following type adopts:

struct CricketRecord: WinLoss {
  var wins: Int
  var losses: Int
  var draws: Int

  var winningPercentage: Double {
    Double(wins) / Double(wins + losses + draws)
  }
}

Observe what happens when you use the winningPercentage property:

let miamiTuples = CricketRecord(wins: 8, losses: 7, draws: 1)
let winLoss: WinLoss = miamiTuples

miamiTuples.winningPercentage // 0.5
winLoss.winningPercentage // 0.53 !!!

Even though miamiTuples and winLoss contain the same instance, you see different results. This result is because static dispatch chooses an implementation based on the compile-time type: CricketRecord for miamiTuples and WinLoss for winLoss.

If you declare winningPercentage as part of the formal WinLoss protocol, the implementation in the extension becomes the default implementation that you can override. In this case, the compiler uses dynamic dispatch, which considers underlying runtime types to call the appropriate method or property.

You’ve seen dynamic dispatch in action in Swift Apprentice: Fundamentals - Chapter 15, “Advanced Classes”, as the dispatch method used for overridden properties and methods in class hierarchies.

Type Constraints

For the protocol extensions on TeamRecord, you could use members of the TeamRecord protocol, such as wins and losses, within the implementations of winningPercentage and gamesPlayed. Much like in a struct, class, or enum extension, you write code as if writing it inside the type you’re extending.

When you write extensions on protocols, there’s an additional dimension to consider: the adopting type could also be any number of other types. In other words, when a type adopts TeamRecord, it might also adopt Comparable, CustomStringConvertible, or even another protocol you wrote yourself!

Swift lets you write extensions for certain adopting types. Using a type constraint on a protocol extension, you can use methods and properties from the type you constrain it to.

Take the following example of a type constraint:

protocol PostSeasonEligible {
  var minimumWinsForPlayoffs: Int { get }
}

extension TeamRecord where Self: PostSeasonEligible {
  var isPlayoffEligible: Bool { 
    wins > minimumWinsForPlayoffs
  }
}

You have a new protocol, PostSeasonEligible, that defines a minimumWinsForPlayoffs property. The magic happens in the extension of TeamRecord, which has a type constraint on Self: PostSeasonEligible that will apply the extension to all adopters of TeamRecord that also adopt PostSeasonEligible.

Applying the type constraint to the TeamRecord extension means that within the extension, self is known to be both a TeamRecord and PostSeasonEligible. That means you can use properties and methods defined on both of those types. You can also use type constraints to create default implementations for different type combinations.

Consider the case of HockeyRecord, which introduced ties in its record along with another implementation of winningPercentage:

struct HockeyRecord: TeamRecord {
  var wins: Int
  var losses: Int
  var ties: Int

  var winningPercentage: Double {
    Double(wins) / Double(wins + losses + ties)
  }
}

Ties are allowed in more games than hockey, so you could make that a protocol instead of coupling it to one specific sport:

protocol Tieable {
  var ties: Int { get }
}

With type constraints, you can also make a default implementation for winningPercentage, specifically for types that are both a TeamRecord and Tieable:

extension TeamRecord where Self: Tieable {
  var winningPercentage: Double {
    Double(wins) / Double(wins + losses + ties)
  }
}

Now, any type that is both a TeamRecord and Tieable won’t need to implement a winningPercentage that factors in ties:

struct RugbyRecord: TeamRecord, Tieable {
  var wins: Int
  var losses: Int
  var ties: Int
}

let rugbyRecord = RugbyRecord(wins: 8, losses: 7, ties: 1)
rugbyRecord.winningPercentage // 0.5

You can provide default implementations that make sense for particular cases using a combination of protocol extensions and constrained protocol extensions.

Mini-Exercise

Write a default implementation on CustomStringConvertible that will print the win/loss record in Wins - Losses format for any TeamRecord type. For instance, if a team is 10 and 5, it should return 10 - 5.

Protocol-Oriented Benefits

What exactly are the benefits of protocol-oriented programming?

Programming to Interfaces, not Implementations

By focusing on protocols instead of implementations, you can apply code contracts to any type — even those that don’t support inheritance. Suppose you were to implement TeamRecord as a base class.

class TeamRecordBase {
  var wins = 0
  var losses = 0

  var winningPercentage: Double {
    Double(wins) / Double(wins + losses)
  }
}
}

Notice what happens if you try to declare BaseballRecord as a struct as you did earlier.

At this point, you’d be stuck working with classes as long as you were working with team records.

If you wanted to add ties to the mix, you’d either have to add ties to your subclass:

class HockeyRecord: TeamRecordBase {
  var ties = 0

  override var winningPercentage: Double {
    Double(wins) / Double(wins + losses + ties)
  }
}

Or you’d have to create yet another base class and thus complicate your class hierarchy:

class TieableRecordBase: TeamRecordBase {
  var ties = 0

  override var winningPercentage: Double {
    Double(wins) / Double(wins + losses + ties)
  }
}

class HockeyRecord: TieableRecordBase {
}

class CricketRecord: TieableRecordBase {
}

Likewise, if you wanted to work with any records that have wins, losses and ties, then you’d generally code against the lowest-common-denominator base class:

extension TieableRecordBase {
  var totalPoints: Int {
    (2 * wins) + (1 * ties)
  }
}

This practice forces you to “code to implementation, not interface.” If you want to compare two teams’ records, you only care about wins and losses. With classes, though, you’d need to operate on the specific base class that happens to define wins and losses.

You don’t want to hear what would happen if you suddenly needed to support divisional wins and losses with some sports! :] With protocols, you don’t need to worry about the specific type or even whether it is a class or a struct; all you care about is the existence of specific common properties and methods.

Traits, Mixins and Multiple Inheritance

Speaking of supporting one-off features such as a divisional win or loss, one of the real benefits of protocols is that they allow a form of multiple inheritance.

When creating a type, you can use protocols to decorate it with all the unique characteristics you want:

protocol TieableRecord {
  var ties: Int { get }
}

protocol DivisionalRecord {
  var divisionalWins: Int { get }
  var divisionalLosses: Int { get }
}

protocol ScoreableRecord {
  var totalPoints: Int { get }
}

extension ScoreableRecord where Self: TieableRecord, Self: TeamRecord {
  var totalPoints: Int {
    (2 * wins) + (1 * ties)
  }
}

struct NewHockeyRecord: TeamRecord, TieableRecord,
       DivisionalRecord, CustomStringConvertible, Equatable {
  var wins: Int
  var losses: Int
  var ties: Int
  var divisionalWins: Int
  var divisionalLosses: Int

  var description: String {
    "\(wins) - \(losses) - \(ties)"
  }
}

NewHockeyRecord is a TeamRecord and a TieableRecord, tracks divisional wins and losses, works with == and defines its own CustomStringConvertible description!

Using protocols this way is described as using traits or mixins. These terms reflect that you can use protocols and protocol extensions to add or mix different behaviors or traits to a type.

Simplicity

When you write a computed property to calculate the winning percentage, you only need wins, losses and ties. When you write code to print a person’s full name, you only need a first and last name.

If you were to write code to do these tasks inside of a more complex object, it could be easy to make the mistake of coupling it with unrelated code:

var winningPercentage: Double {
  var percent = Double(wins) / Double(wins + losses)

  // Oh no! Not relevant!
  above500 = percent > 0.5

  return percent
}

That above500 property might be needed in cricket but not hockey. However, that makes the function very specific to a particular sport.

You saw how simple the protocol extension version of this function was: It handled one calculation only. It lets you leverage a default implementation kept in one place.

You don’t need to know that the type adopting a protocol is a HockeyRecord, or a StudentAthlete, or a class, struct or enum. Because the code inside your protocol extension operates only on the protocol itself, any type that conforms to that protocol will be able to leverage this code.

You’ll repeatedly discover in your coding life that simpler code is less buggy code.

Why Swift is a Protocol-Oriented Language

You’ve learned about the capabilities of protocols and protocol extensions, but you may be wondering: What exactly does it mean that Swift is a protocol-oriented language?

Protocol extensions significantly affect your ability to write expressive and decoupled code — and the Swift standard library uses protocol extensions extensively.

To begin with, you can contrast protocol-oriented programming with object-oriented programming. The latter focuses on the idea of mutable objects and how objects interact. Because of this, the class is at the center of any object-oriented language.

Though classes are a part of Swift, you’ll find they are an extremely small part of the standard library. Instead, the Swift standard library is value types (or types with value semantics) that conform to protocols. You can see the significance in many of Swift’s core types, such as Int and Array. Consider the definition of Array:

// From the Swift standard library
public struct Array<Element> : RandomAccessCollection, MutableCollection {
  // ...
}

The fact that Array is a struct means it’s a value type, of course, but it also means that it can’t be subclassed, nor can it be a superclass. Instead of inheriting behaviors from common base classes, Array adopts protocols to define many of its more common capabilities.

Array is a MutableCollection and a Collection. Thanks to protocol extensions, Array will get numerous properties and methods common to every Collection, such as first, count or isEmpty — simply by conforming to Collection.

Thanks to protocol extensions with generic constraints, you can split() an Array or find the index(of:) an element, assuming the type of that element conforms to Equatable.

These implementations are all defined within protocol extensions in the Swift standard library. By implementing them in protocol extensions, these behaviors can be treated as mixins and don’t need to be explicitly reimplemented on each adopting type.

This decoration of defined behaviors lets Array and Dictionary — yet another Collection — be similar in some respects and different in others. Had Swift used subclassing, Dictionary and Array would either share one common base class or none at all. With protocols and protocol-oriented programming, you can treat them both as a Collection.

With a design centered around protocols rather than specific classes, structs or enums, your code is instantly more portable and decoupled — methods now apply to a range of types instead of a particular type. Your code is also more cohesive because it operates only on the properties and methods within the protocol you’re extending and its type constraints. And it ignores the internal details of any type that conforms to it.

Understanding protocol-oriented programming is a powerful skill that will help you become a better Swift developer and give you new ways to think about how to design your code.

Note: More neutral-minded Swift developers will call Swift a “multi-paradigm” language. You’ve already seen inheritance, object-oriented techniques, and protocol-oriented programming; Swift easily handles all of them!

Protocols and protocol-oriented programming are at the foundation of the Swift language. The generics system, for example, uses protocols to specify with precision the type requirements of a generic type in use. If you have m data structures and n algorithms that operate on those data structures, in some languages, you need m * n blocks of code to implement them. With Swift, using protocols, you only need to write m + n blocks with no repetition. Protocol-oriented programming gives you all the advantages of object-oriented programming while dodging most pitfalls.

Next time you face a programming task, start with value types. See if you can figure out common elements across types. These become candidates for protocols and are often deeply connected with the problem domain of concern. Thinking this way may lead you to a more flexible and extensible solution. Just as Neo can see the red dress in “The Matrix”, the more you get into this habit, the easier it will be to see protocol abstractions.

Challenges

Before moving on, here are some challenges to test your knowledge of protocol-oriented programming. It’s best to try to solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.

Challenge 1: Protocol Extension Practice

Suppose you own a retail store. You have food items, clothes and electronics. Begin with an Item protocol:

protocol Item {
  var name: String { get }
  var clearance: Bool { get }
  var msrp: Double { get } // Manufacturer’s Suggested Retail Price
  var totalPrice: Double { get }
}

Fulfill the following requirements using what you’ve learned about protocol-oriented programming. In other words, minimize the code in your classes, structs or enums.

  • Clothes don’t have a sales tax, but all other items have a 7.5% sales tax.
  • When food items are discounted 50% on clearance, clothes are discounted 25%, and electronics are discounted 5%.
  • Items should implement CustomStringConvertible and return name. Food items should also print their expiration dates.

Challenge 2: Doubling Values

Write a protocol extension on Sequence named double() that only applies to sequences of numeric elements. Make it return an array where each element is twice the element in the sequence. Test your implementation on an array of Int and an array of Double, then see if you can try it on an array of String. Hints:

  • Numeric values implement the protocol Numeric.
  • Your method signature should be double() -> [Element]. The type [Element] is an array of whatever type the Sequence holds, such as String or Int.

Key Points

  • Protocol extensions let you write implementation code for protocols and even write default implementations on methods required by a protocol.
  • Protocol extensions are the primary driver for protocol-oriented programming and let you write code that will work on any type that conforms to a protocol.
  • Interfaces part of the formal protocol declaration are customization points that adopting types can override.
  • Type constraints on protocol extensions provide additional context and let you write more specialized implementations.
  • You can decorate a type with traits and mixins to extend behavior without requiring inheritance.
  • Protocols, when used well, promote code reuse and encapsulation.
  • Start with value types and find the fundamental protocols.
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.
© 2026 Kodeco Inc.