Update Note: This tutorial has been updated to Swift 3.0 by Niv Yahel. The original tutorial was written by Erik Kerber.
Imagine you’re developing a racing game. You can drive a car, ride a motorcycle, or even fly a plane. A common approach to creating this type of application is by using object oriented design, encapsulating all of the logic inside of an object that gets inherited to all of those that share similarity.
This design approach works, but does come with some drawbacks. For example, if you add the ability to create machines that also require gas, birds that fly in the background, or anything else that may want to share game logic, there isn’t a good way to separate the functional components of vehicles into something reusable.
This scenario is where protocols really shine.
Swift has always let you specify interface guarantees on existing class
, struct
and enum
types using protocols. This lets you interact with them generically. Swift 2 introduced a way to extend protocols and provide default implementations. Finally, Swift 3 improves operator conformance and uses these improvements for the new numeric protocols in the standard library.
Protocol are extremely powerful and can transform the way you write code. In this tutorial, you’ll explore the ways you can create and use protocols, as well as use protocol-oriented programming patterns to make your code more extensible.
You’ll also see how the Swift team was able to use protocol extensions to improve the Swift standard library itself, and how it impacts the code you write.
Getting Started
Begin by creating a new playground. In Xcode, select File\New\Playground… and name the playground SwiftProtocols. You can select any platform, since all the code in this tutorial is platform-agnostic. Click Next to choose where you would like to save it, and finally click Create.
Once your new playground is open, add the following code to it:
protocol Bird { var name: String { get } var canFly: Bool { get } } protocol Flyable { var airspeedVelocity: Double { get } } |
This defines a simple protocol Bird
with properties name
and canFly
, as well as a Flyable
protocol which defines airspeedVelocity
.
In a pre-protocol world, you might have started with Flyable
as a base class and then relied on object inheritance to define Bird
as well as other things that fly, such as airplanes. Note that here, everything is starting out as a protocol! This allows you to encapsulate the functional concept in a way that doesn’t require a base class.
You’ll see how this makes the entire system more flexible when you start to define actual types next.
Defining Protocol-Conforming Types
Add the following struct
definition to the bottom of the playground:
struct FlappyBird: Bird, Flyable { let name: String let flappyAmplitude: Double let flappyFrequency: Double let canFly = true var airspeedVelocity: Double { return 3 * flappyFrequency * flappyAmplitude } } |
This defines a new struct FlappyBird
, which conforms to both the Bird
and Flyable
protocols. Its airspeedVelocity
is calculated as a function of flappyFrequency
and flappyAmplitude
. Being flappy, it returns true
for canFly
. :]
Next, add the following two struct definitions to the bottom of the playground:
struct Penguin: Bird { let name: String let canFly = false } struct SwiftBird: Bird, Flyable { var name: String { return "Swift \(version)" } let version: Double let canFly = true // Swift is FASTER every version! var airspeedVelocity: Double { return version * 1000.0 } } |
A Penguin
is a Bird
, but cannot fly. A-ha — it’s a good thing you didn’t take the inheritance approach, and make all birds flyable after all! Using protocols allows you to define functional components and have any relevant object conform to them.
Already you can see some redundancies. Every type of Bird
has to declare whether it canFly
or not, even though there’s already a notion of Flyable
in your system.
Extending Protocols With Default Implementations
With protocol extensions, you can define default behavior for a protocol. Add the following just below the Bird
protocol definition:
extension Bird { // Flyable birds can fly! var canFly: Bool { return self is Flyable } } |
This defines an extension on Bird
that sets the default behavior for canFly
to return true
whenever the type is also Flyable
. In other words, any Flyable
bird no longer needs to explicitly declare so!
Delete the let canFly = ...
from FlappyBird
, SwiftBird
and Penguin
struct declarations. You’ll see that the playground successfully builds since the protocol extension now handles that requirement for you.
Why Not Base Classes?
Protocol extensions and default implementations may seem similar to using a base class or even abstract classes in other languages, but they offer a few key advantages in Swift:
- Because types can conform to more than one protocol, they can be decorated with default behaviors from multiple protocols. Unlike multiple inheritance of classes which some programming languages support, protocol extensions do not introduce any additional state.
- Protocols can be adopted by classes, structs and enums. Base classes and inheritance are restricted to class types.
In other words, protocol extensions provide the ability to define default behavior for value types and not just classes.
You’ve already seen this in action with a struct. Next, add the following enum definition to the end of the playground:
enum UnladenSwallow: Bird, Flyable { case african case european case unknown var name: String { switch self { case .african: return "African" case .european: return "European" case .unknown: return "What do you mean? African or European?" } } var airspeedVelocity: Double { switch self { case .african: return 10.0 case .european: return 9.9 case .unknown: fatalError("You are thrown from the bridge of death!") } } } |
As with any other value type, all you need to do is define the correct properties so UnladenSwallow
conforms to the two protocols. Because it conforms to both Bird
and Flyable
. It also gets the default implementation for canFly
!
Did you really think this tutorial involving airspeedVelocity
wouldn’t include a Monty Python reference? :]
Overriding Default Behavior
Your UnladenSwallow
type automatically got an implementation for canFly
by virtue of conforming to the Bird
protocol. However, you really want UnladenSwallow.unknown
to return false
for canFly
. Is it possible to override the default implementation? Yes, it is. Add this to the end of your playground.
extension UnladenSwallow { var canFly: Bool { return self != .unknown } } |
Now only .african
and .european
will return true
for canFly
. Test it out by adding the following to the end of your playground:
UnladenSwallow.unknown.canFly // false UnladenSwallow.african.canFly // true Penguin(name: "King Penguin").canFly // false |
In this way, it is possible to override properties and methods much like you can with virtual methods in object oriented programming.
Extending Protocols
You can utilize protocols from the standard library and also define default behaviors.
Modify the Bird
protocol declaration to conform to the CustomStringConvertible
protocol:
protocol Bird: CustomStringConvertible { |
Conforming to CustomStringConvertible
means your type needs to have a description
property so it acts like a String. Does that mean you now have to add this property to every current and future Bird
type?
Of course, there’s an easier way with protocol extensions. Add the code underneath the Bird
definition:
extension CustomStringConvertible where Self: Bird { var description: String { return canFly ? "I can fly" : "Guess I’ll just sit here :[" } } |
This extension will make the canFly
property represent each Bird
type’s description
value.
To try it out, add the following to the bottom of the playground:
UnladenSwallow.african |
You should see “I can fly!”
appear in the assistant editor. But more notably, you just extended your own protocol!
Effects on the Swift Standard Library
You’ve seen how protocol extensions are a great way to customize and extend the capabilities. What may surprise you is how the Swift team was able to use protocols to improve the way the Swift standard library is written as well.
Add the following code to the end of your playground:
let numbers = [10,20,30,40,50,60] let slice = numbers[1...3] let reversedSlice = slice.reversed() let answer = reversedSlice.map { $0 * 10 } print(answer) |
This should look pretty straightforward, and you might even be able to guess the answer that is printed. What might be surprising are the types involved. slice
, for example, is not an Array
of integers but an ArraySlice<Int>
. This special wrapper type acts as a view into the original array and avoids costly memory allocations that can quickly add up. Similarly, reversedSlice
is actually a ReversedRandomAccessCollection<ArraySlice<Int>>
which is again just a wrapper type view into the original array.
Fortunately, the geniuses developing the standard library defined the map
method as an extension to the Sequence
protocol and all of the collection wrappers (of which there are dozens) to conform to this protocol. This makes it possible to call map on Array
just as easily as it is ReversedRandomAccessCollection
and not notice the difference. You will borrow this important design pattern shortly.
Off to the Races
So far you defined several Bird
conforming types. Now add something totally different to the end of your playground.
class Motorcycle { init(name: String) { self.name = name speed = 200 } var name: String var speed: Double } |
This class that has nothing to do with birds or flying things you have defined so far. But you want to race motorcycles as well as penguins. Time to bring all of the pieces together.
Bringing it Together
It is time to unify all of these disparate types with a common protocol for racing. You can do this with out even going back and touching the original model definitions. The fancy term for this is retroactive modeling. Just add the following to your playground:
protocol Racer { var speed: Double { get } // speed is the only thing racers care about } extension FlappyBird: Racer { var speed: Double { return airspeedVelocity } } extension SwiftBird: Racer { var speed: Double { return airspeedVelocity } } extension Penguin: Racer { var speed: Double { return 42 // full waddle speed } } extension UnladenSwallow: Racer { var speed: Double { return canFly ? airspeedVelocity : 0 } } extension Motorcycle: Racer {} let racers: [Racer] = [UnladenSwallow.african, UnladenSwallow.european, UnladenSwallow.unknown, Penguin(name: "King Penguin"), SwiftBird(version: 3.0), FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0), Motorcycle(name: "Giacomo") ] |
In this code, you first define the protocol Racer
and then you make all of the different types conform. Some types, such as Motorcycle
conform trivially. Others, such as UnladenSwallow
need a bit more logic. In the end, you have a bunch of conforming Racer
types.
With all of the types conforming, you then create an array of racers.
Top Speed
Now it’s time to write a function that determines the top speed of the racers. Add this to the end of your playground:
func topSpeed(of racers: [Racer]) -> Double { return racers.max(by: { $0.speed < $1.speed })?.speed ?? 0 } topSpeed(of: racers) // 3000 |
This function uses the standard library max
to find the racer with the largest speed and return that. You return 0 if the user passes in an empty array in for racers
.
Looks like it’s Swift 3 FTW. As if it were ever in doubt! :]
Making it more generic
There is a problem though. Suppose you want to find the top speed for a subset (slice) of racers
. Adding this to your playground you get an error:
topSpeed(of: racers[1...3]) // ERROR |
Swift complains it cannot subscript a value of type [Racer]
with an index of type CountableClosedRange
. Slicing returns one of those wrapper types.
The solution is to write your code against a common protocol instead of the concrete Array
. Add the following before the topSpeed(of:)
call.
func topSpeed<RacerType: Sequence>(of racers: RacerType) -> Double where RacerType.Iterator.Element == Racer { return racers.max(by: { $0.speed < $1.speed })?.speed ?? 0 } |
This might look a bit scary, so let’s break it down. RacerType
is the generic type for this function and it can be any type that conforms to the Swift standard library’s Sequence
protocol. The where
clause specifies that the element type of the sequence must conform to your Racer
protocol. All Sequence
types have an associated type named Iterator
that can loop through types of Element
. The actual method body is mostly the same as before.
This method works for any Sequence
type including array slices.
topSpeed(of: racers[1...3]) // 42 |
Making it More Swifty
You can do even a little better. Borrowing from the standard library play book, you can extend Sequence
type itself so that topSpeed()
is readily discoverable. Add the following to the end of your playground:
extension Sequence where Iterator.Element == Racer { func topSpeed() -> Double { return self.max(by: { $0.speed < $1.speed })?.speed ?? 0 } } racers.topSpeed() // 3000 racers[1...3].topSpeed() // 42 |
Now you have a method that is easily discoverable but only applies (and autocompletes) when you are dealing with sequences of racers.
Protocol Comparators
One Swift 3 improvement to protocols is how you create operator requirements.
Add the following to the bottom of the playground:
protocol Score { var value: Int { get } } struct RacingScore: Score { let value: Int } |
Having a Score
protocol means that you can write code that treats all scores the same way. However, by having different concrete types such as RacingScore
you are sure not to mix up these scores with style scores or cuteness scores. Thanks compiler!
You really want scores to be comparable so you can tell who has the high score. Before Swift 3, you needed to add global operator functions to conform to these protocols. Now you can define these static method that is part of the model. Do so now by replacing the definition of Score
and RacingScore
with the following:
protocol Score: Equatable, Comparable { var value: Int { get } } struct RacingScore: Score { let value: Int static func ==(lhs: RacingScore, rhs: RacingScore) -> Bool { return lhs.value == rhs.value } static func <(lhs: RacingScore, rhs: RacingScore) -> Bool { return lhs.value < rhs.value } } |
You just encapsulated all of the logic for RacingScore
in one place. Now you can compare scores, and, with the magic of protocol extension default implementations, even use operators such as greater-than-or-equal-to that you never explicitly defined.
RacingScore(value: 150) >= RacingScore(value: 130) // true |
Where To Go From Here?
You can download the complete playground with all the code in this tutorial
here.
You’ve seen the power of protocol-oriented programming by creating your own simple protocols and extending them using protocol extensions. With default implementations, you can give existing protocols common and automatic behavior, much like a base class but better since it can apply to structs and enums too.
In addition, protocol extensions can not only be used to extend your own protocols, but can extend and provide default behavior to protocols in the Swift standard library, Cocoa, Cocoa Touch, or any third party library.
To continue learning more about protocols, you should read the official Apple documentation.
You can view an excellent WWDC session on Protocol Oriented Programming on Apple’s developer portal for a more in-depth look into the theory behind it all.
The rationale for operator conformance can be found on the Swift evolution proposal. You might also want to learn more about Swift collection protocols and learn how to build your own.
Finally, as with any “new” programming paradigm, it is easy to get overly exuberant and use it for all the things. This interesting blog post by Chris Eidhof reminds us that we should beware of silver bullet solutions and using protocols everywhere “just because”.
Have any questions? Let us know in the forum discussion below!
The post Introducing Protocol-Oriented Programming in Swift 3 appeared first on Ray Wenderlich.