The following is from Apple's Swift Programming Language book:
"A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol."
– Apple Inc., The Swift Programming Language (Swift 3.0.1), iBooks
Protocol-oriented programming is a vast topic that also deserves coverage. It is the subject of many discussions, and I won't dive into it in depth. However, let's go over the basic concepts, as they will be useful for understanding some concepts that will be explained later in this book.
You declare protocols using the protocol
keyword, as follows:
protocol Toggling { mutating func toggle() }
Now that this protocol has been declared, any time that you declare a type conforming to Toggling
, you'll be required to implement a mutating toggle()
function.
You can use protocols in your type declarations, method declarations, or variable declarations. While it is technically possible to use protocols as interfaces for your objects or structs, that is usually not how they are used in Swift. Often, you will find yourself conforming to protocols when declaring your custom types or later in your code base, part of extending your existing type to bring additional functionality to it.
We have just declared this new toggling protocol. If we go back to the previous section about enums, you may remember that the State
enum had a toggle()
method. We can now declare that our enum
, State
, conforms to Toggling
. As mentioned previously, we have many ways to declare our conformance.
The first method to declare a conformance is to do it at the top level, when you declare your custom type. You'll notice that the raw representation comes first, then the protocol conformance:
enum State: Int, Toggling { case off = 0 case on mutating func toggle() { self = self == .on ? .off : .on } } var state: State = .on state.toggle() assert(state == .off)
The second way to declare a conformance is to add the conformance to an extension. The main benefit is that you can add functionalities, in the form of extensions, to existing types. The other main benefit of declaring a conformance inside of an extension is that you can scope this conformance to a particular file or module, with the private
modifier.
For example, suppose that we want to add the toggle
method to the Bool
type, but only for the current file or your framework. You may not want it to leak outside, as the implementation may conflict with another one:
internal extensionBool: Toggling { mutatingfunc toggle() { self = !self } } var isReady = false isReady.toggle() assert(isReady)
With protocol extensions, it is possible to provide an implementation for the required methods, without letting the conforming types provide that implementation.
We have updated the Toggling
protocol with an additional required member: isActive
. With the protocol extension, we can declare a default implementation for our types, Bool
and State
. We can also provide a default implementation for any other type that would choose to conform to the Toggling
protocol:
protocol Toggling { mutating func toggle() var isActive: Bool { get } } extensionTogglingwhereSelf == Bool { var isActive: Bool { returnself } } extensionTogglingwhereSelf == State { var isActive: Bool { returnself == .on } }
It is possible to provide default implementations for protocols through extensions. Previously, we provided a partial default implementation for the Toggling
protocol on a well-known type. But any other type, that would conform toToggling
needs to provide an implementation on isActive
. Using another example, let's look at how we can leverage default implementations in protocol extensions without requiring additional conformance work.
Let's work with a simple protocol, Adder
, for the sake of the example:
protocol Adder { func add(value: Int) -> Int func remove(value: Int) -> Int }
The Adder
protocol declares two methods: add
and remove
. And, if we remember our math classes well, we can very well declare remove
as a function of add
. Removing is just adding a negative value. Protocol extension allows us to do just that:
extensionAdder { func remove(value: Int) -> Int { returnadd(value: -value) } }
This may look a bit silly, but in reality, this pattern is really powerful. Remember, we were able to implement remove
because we were able to express it as a function of another provided method. Often, in our code, we can implement a method as a function of another. Protocols give us a contract that is fulfilled by either the concrete type or the extension, and we can effectively and expressively compose our programs around those capabilities.