Swift protocols play a leading role in the structure of standard library and a common method of abstraction. They provide a similar experience to interfaces that some other languages have.
Protocol-oriented programming has become somewhat a fundamental in Swift. It is important you need to grasp following:
is
, as
)@objc optional
)Protocols allow you to group similar methods, functions and properties. Swift lets you specify these interface guarantees on class, struct and enum types. Only class types can use base classes and inheritance.
An advantage of protocols in Swift is that objects can conform to multiple protocols.
When writing an app this way, your code becomes more modular. Think of protocols as building blocks of functionality. When you add new functionality by conforming an object to a protocol, you don’t build a whole new object. That’s time-consuming. Instead, you add different building blocks until your object is ready.
Protocol extensions may seem quite similar to using a base class, but there are several benefits of using protocol extensions. These include, but are not necessarily limited to:
Here is a list of advantages of protocols and how it differs with inheritance:
Features | Inheritance | Protocols |
---|---|---|
Interface reuse | Inherit from superclass | Protocol extensions |
Customization | Overriding + while maintaining invariants | Implement requirements + override defaults |
Implementation reuse | Inherit from superclass | Adopt protocols |
Usable with value types | No | Yes |
Modeling flexibility | Upfront modeling + exclusive hierachies | Retroactive modeling + ad-hoc hierachies |
Rather than having to lock ourselves into inflexible inheritance hierarchies and being forced to settle on key abstractions early in the design process, Swift protocols and protocol extensions enable retroactive modeling. We can start with concrete code and work our way to abstractions when we need to assign a role or a persona to entities or concepts in the domain.
Retroactive modeling is the practice of using existing types to represent new concepts, without modifying those types. This technique is important for reusing existing structures, while maintaining compatibility with the current usage. Swift supports retroactive modeling through the use of extensions.
Extensions enable you to add new functionality to existing types, without the need to have access to the original source code. Swift extensions are similar to categories in Objective-C, and can be used to extend a class, struct, enum, or protocol.
Retroactive modeling saves us from having to decide on key abstractions early in the design process.
It can be useful to require a type to conform to multiple protocols at the same time. You can combine multiple protocols into a single requirement with a protocol composition. Protocol compositions behave as if you defined a temporary local protocol that has the combined requirements of all protocols in the composition. Protocol compositions don’t define any new protocol types.
Protocol compositions have the form SomeProtocol & AnotherProtocol
. You can list as many protocols as you need, separating them with ampersands (&). In addition to its list of protocols, a protocol composition can also contain one class type, which you can use to specify a required superclass.
protocol Player {
func play()
}
protocol Stoppable {
func stop()
}
class MusicPlayer: Player, Stoppable {
func play() {
print("Playing music...")
}
func stop() {
print("Stopping music...")
}
}
class VideoPlayer: Player, Stoppable {
func play() {
print("Playing video...")
}
func stop() {
print("Stopping video...")
}
}
let musicPlayer = MusicPlayer()
let videoPlayer = VideoPlayer()
// Create a protocol composition of Player and Stoppable
var playerAndStoppable: Player & Stoppable = musicPlayer
// Call methods on the protocol composition
playerAndStoppable.play() // Output: Playing music...
playerAndStoppable.stop() // Output: Stopping music...
// Update the protocol composition to use the VideoPlayer
playerAndStoppable = videoPlayer
// Call methods on the updated protocol composition
playerAndStoppable.play() // Output: Playing video...
playerAndStoppable.stop() // Output: Stopping video...
This example demonstrates the power of protocol oriented in Swift. By defining behavior in a protocol and implementing it in a type-specific way, we can create flexible and extensible code that’s easy to maintain and test. We can also take advantage of Swift’s support for protocol extensions to provide default implementations for shared behavior, further reducing code duplication.
protocol Animal {
var name: String { get }
var numberOfLegs: Int { get }
func makeSound()
}
extension Animal {
func makeSound() {
print("Unknown sound")
}
}
struct Dog: Animal {
var name: String
var numberOfLegs: Int
func makeSound() {
print("Woof!")
}
}
struct Cat: Animal {
var name: String
var numberOfLegs: Int
func makeSound() {
print("Meow!")
}
}
let dog = Dog(name: "Fido", numberOfLegs: 4)
let cat = Cat(name: "Whiskers", numberOfLegs: 4)
dog.makeSound() // Output: Woof!
cat.makeSound() // Output: Meow!
In this example, we define a protocol called Animal
that defines two properties (name
and numberOfLegs
) and a method (makeSound()
). We also provide a default implementation for makeSound()
in an extension of the Animal
protocol, which prints "Unknown sound"
.
We then define two structs (Dog
and Cat
) that conform to the Animal
protocol and provide their own implementation of makeSound()
. Finally, we create instances of Dog
and Cat
and call their makeSound()
methods, which print "Woof!"
and "Meow!"
, respectively.