Introduction
With the release of Swift 2, Apple added a range of new features and capabilities to the Swift programming language. One of the most important, however, was an overhaul of protocols. The improved functionality available with Swift protocols allows for a new type of programming, protocol-oriented programming. This is in contrast to the more common object-orientated programming style many of us are used to.
In this tutorial, I am going to show you the basics of protocol-oriented programming in Swift and how it differs from object-orientated programming.
Prerequisites
This tutorial requires that you are running Xcode 7 or higher, which includes support for version 2 of the Swift programming language.
1. Protocol Basics
If you aren't already familiar with protocols, they are a way of extending the functionality of an existing class or structure. A protocol can be thought of as a blueprint or interface that defines a set of properties and methods. A class or structure that conforms to a protocol is required to fill out these properties and methods with values and implementations respectively.
It should also be noted that any of these properties and methods can be designated as optional, which means that conforming types aren't required to implement them. A protocol definition and class conformance in Swift could look like this:
protocol Welcome { var welcomeMessage: String { get set } optional func welcome() } class Welcomer: Welcome { var welcomeMessage = "Hello World!" func welcome() { print(welcomeMessage) } }
2. An Example
To begin, open Xcode and create a new playground for either iOS or OS X. Once Xcode has created the playground, replace its contents with the following:
protocol Drivable { var topSpeed: Int { get } } protocol Reversible { var reverseSpeed: Int { get } } protocol Transport { var seatCount: Int { get } }
We define three protocols, each containing a property. Next, we create a structure that conforms to these three protocols. Add the following code to the playground:
struct Car: Drivable, Reversible, Transport { var topSpeed = 150 var reverseSpeed = 20 var seatCount = 5 }
You may have noticed that instead of creating a class that conforms to these protocols, we created a structure. We do this to avoid one of the typical problems inherent to object-oriented programming, object references.
Imagine, for example, that you have two objects, A and B. A creates some data on its own and keeps a reference to that data. A then shares this data with B by reference, which means that both objects have a reference to the same object. Without A knowing, B changes the data in some way.
While this may not seem like a big problem, it can be when A did not expect the data to be altered. Object A may find data it doesn't know how to handle or deal with. This is a common risk of object references.
In Swift, structures are passed by value rather than by reference. This means that, in the above example, if the data created by A was packaged as a structure instead of an object and shared with B, the data would be copied instead of shared by reference. This would then result in both A and B having their own unique copy of the same piece of data. A change made by B wouldn't affect the copy managed by A.
Breaking up the Drivable
, Reversible
, and Transport
components into individual protocols also allows for greater level of customization than traditional class inheritance. If you've read my first tutorial about the new GameplayKit framework in iOS 9, then this protocol-oriented model is very similar to the Entities and Components structure used in the GameplayKit framework.
By adopting this approach, custom data types can inherit functionality from multiple sources rather than a single superclass. Keeping in mind what we've got so far, we could create the following classes:
- a class with components of the
Drivable
andReversible
protocols - a class with components of the
Drivable
andTransportable
protocols - a class with components of the
Reversible
andTransportable
protocols
With object-oriented programming, the most logical way to create these three classes would be to inherit from one superclass that contains the components of all three protocols. This approach, however, results in the superclass being more complicated than it needs to be and each of the subclasses inheriting more functionality than it needs.
3. Protocol Extensions
Everything I showed you so far has been possible in Swift ever since its release in 2014. These same protocol-oriented concepts could have even been applied to Objective-C protocols. Due to the limitations that used to exist on protocols, however, true protocol-oriented programming wasn't possible until a number of key features were added to the Swift language in version 2. One of the most important of these features is protocol extensions, including conditional extensions.
Firstly, let's extend the Drivable
protocol and add a function to determine whether or not a particular Drivable
is faster than another. Add the following to your playground:
extension Drivable { func isFasterThan(item: Drivable) -> Bool { return self.topSpeed > item.topSpeed } } let sedan = Car() let sportsCar = Car(topSpeed: 250, reverseSpeed: 25, seatCount: 2) sedan.isFasterThan(sportsCar)
You can see that, when the playground's code is executed, it outputs a value of false
as your sedan
car has a default topSpeed
of 150
, which is less than the sportsCar
.
You may have noticed that we provided a function definition rather than a function declaration. This seems strange, because protocols are only supposed to contain declarations. Right? This is another very important feature of protocol extensions in Swift 2, default behaviors. By extending a protocol, you can provide a default implementation for functions and computed properties so that classes conforming to the protocol don't have to.
Next, we are going to define another Drivable
protocol extension, but this time we'll only define it for value types that also conform to the Reversible
protocol. This extension will then contain a function that determines which object has the better speed range. We can achieve this with the following code:
extension Drivable where Self: Reversible { func hasLargerRangeThan(item: Self) -> Bool { return (self.topSpeed + self.reverseSpeed) > (item.topSpeed + item.reverseSpeed) } } sportsCar.hasLargerRangeThan(sedan)
The Self
keyword, spelled with a capital "S", is used to represent the class or structure that conforms to the protocol. In the above example, the Self
keyword represents the Car
structure.
After running the playground's code, Xcode will output the results in the sidebar on the right as shown below. Note that sportsCar
has a larger range than sedan
.
4. Working With the Swift Standard Library
While defining and extending your own protocols can be very useful, the true power of protocol extensions shows when working with the Swift standard library. This allows you to add properties or functions to existing protocols, such as CollectionType
(used for things like arrays and dictionaries) and Equatable
(being able to determine when two objects are equal or not). With conditional protocol extensions, you can also provide very specific functionality for a specific type of object that conforms to a protocol.
In our playground, we are going to extend the CollectionType
protocol and create two methods, one to get the average top speed of cars in a Car
array and another for the average reverse speed. Add the following code to your playground:
extension CollectionType where Self.Generator.Element: Drivable { func averageTopSpeed() -> Int { var total = 0, count = 0 for item in self { total += item.topSpeed count++ } return (total/count) } } func averageReverseSpeed<T: CollectionType where T.Generator.Element: Reversible>(items: T) -> Int { var total = 0, count = 0 for item in items { total += item.reverseSpeed count++ } return (total/count) } let cars = [Car(), sedan, sportsCar] cars.averageTopSpeed() averageReverseSpeed(cars)
The protocol extension that defines the averageTopSpeed
method takes advantage of conditional extensions in Swift 2. In contrast, the averageReverseSpeed
function we define directly below it is another way to achieve a similar result utilizing Swift generics. I personally prefer the cleaner looking CollectionType
protocol extension, but it's up to personal preference.
In both functions, we iterate through the array, add up the total amount, and then return the average value. Note that we manually keep a count of the items in the array, because when working with CollectionType
rather than regular Array
type items, the count
property is a Self.Index.Distance
type value rather than an Int
.
Once your playground has executed all of this code, you should see an output average top speed of 183
and an average reverse speed of 21
.
5. Importance of Classes
Despite protocol-oriented programming being a very efficient and scalable way to manage your code in Swift, there are still perfectly valid reasons for using classes when developing in Swift:
Backwards Compatibility
The majority of the iOS, watchOS, and tvOS SDKs are written in Objective-C, using an object-oriented approach. If you need to interact with any of the APIs included in these SDKs, you are forced to use the classes defined in these SDKs.
Referencing an External File or Item
The Swift compiler optimizes the lifetime of objects based on when and where they are used. The stability of class-based objects means that your references to other files and items will remain consistent.
Object References
Object references are exactly what you need at times, for example, if you're feeding information into a particular object, such as a graphics renderer. Using classes with implicit sharing is important in situations like this, because you need to be sure that the renderer you are sending the data to is still the same renderer as before.
Conclusion
Hopefully by the end of this tutorial you can see the potential of protocol-oriented programming in Swift and how it can be used to streamline and extend your code. While this new methodology of coding will not entirely replace object-oriented programming, it does bring a number of very useful, new possibilities.
From default behaviors to protocol extensions, protocol-oriented programming in Swift is going to be adopted by many future APIs and will completely change the way in which we think about software development.
As always, be sure to leave your comments and feedback in the comments below.
Comments