Protocols in Swift
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.
protocol SomeProtocol {
// protocol definition goes here
}
struct SomeStructure: FirstProtocol, AnotherProtocol {
// structure definition goes here
}
// If a class has a superclass, list the superclass name before any protocols it adopts, followed by a comma
SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// class definition goes here
}
Property Requirements
get
and set
are keywords used in protocols to define properties, which are values that can be read and/or modified.
When defining a property in a protocol, the get
keyword is used to define the property's getter, which returns the value of the property. The set
keyword is used to define the property's setter, which sets a new value for the property.
Here’s an example of a protocol with a property that has a getter and a setter:
protocol Vehicle {
var speed: Double { get set }
}
class Car: Vehicle {
var speed: Double = 0.0
}
let myCar = Car()
myCar.speed = 60.0
print(myCar.speed) // Output: 60.0
In this example, the Vehicle
protocol defines a property called speed
, which is of type Double
. The get
and set
keywords are used to define the property's getter and setter, respectively.
When a class, struct, or enum adopts the Vehicle
protocol, it must implement the speed
property with both a getter and a setter.
In this example, the Car
class implements the speed
property with both a getter and a setter. The initial value of speed
is set to 0.0
.
In summary, get
and set
are used in protocols to define properties with getters and setters, respectively. When a class, struct, or enum adopts a protocol with a property, it must implement the property with both a getter and a setter.
General examples:
protocol FullyNamed {
var fullName: String { get }
}
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"
class Starship: FullyNamed {
var prefix: String?
var name: String
init(name: String, prefix: String? = nil) {
self.name = name
self.prefix = prefix
}
var fullName: String {
return (prefix != nil ? prefix! + " " : "") + name
}
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"
Method Requirements
Protocols can require specific instance methods and type methods to be implemented by conforming types. These methods are written as part of the protocol’s definition in exactly the same way as for normal instance and type methods, but without curly braces or a method body. Variadic parameters are allowed, subject to the same rules as for normal methods. Default values, however, can’t be specified for method parameters within a protocol’s definition.
Examples:
protocol SomeProtocol {
static func someTypeMethod()
}
protocol RandomNumberGenerator {
func random() -> Double
}
class LinearCongruentialGenerator: RandomNumberGenerator {
var lastRandom = 42.0
let m = 139968.0
let a = 3877.0
let c = 29573.0
func random() -> Double {
lastRandom = ((lastRandom * a + c)
.truncatingRemainder(dividingBy:m))
return lastRandom / m
}
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"
Mutating Method Requirements
In Swift, a protocol can be marked as mutating
to allow its methods to modify the conforming instance. This is useful when you want to define a protocol that can be adopted by value types (such as structs and enums) and you need to modify their properties.
It’s sometimes necessary for a method to modify (or mutate) the instance it belongs to. For instance methods on value types (that is, structures and enumerations) you place the mutating
keyword before a method’s func
keyword to indicate that the method is allowed to modify the instance it belongs to and any properties of that instance. This process is described in Modifying Value Types from Within Instance Methods.
NOTE:
If you mark a protocol instance method requirement as mutating
, you need to use the mutating
keyword when implementing that method for a class.
The mutating
keyword is used to indicate that the implementation of the method may modify the properties of the class instance that it is called on. It is necessary to use the mutating
keyword because classes are reference types and their properties can be modified without changing their identity. This is different from value types like structs and enums, where properties cannot be modified without creating a new instance.
So, when you implement a mutating
instance method requirement of a protocol for a class, you must use the mutating
keyword to indicate that the method may modify the state of the class instance.
protocol Vehicle {
var currentSpeed: Double { get set }
mutating func accelerate(by speed: Double)
}
struct Car: Vehicle {
var currentSpeed: Double = 0.0
mutating func accelerate(by speed: Double) {
currentSpeed += speed
}
}
var myCar = Car()
myCar.accelerate(by: 50.0)
print(myCar.currentSpeed) // Output: 50.0
Initializer Requirements
Protocols can require specific initializers to be implemented by conforming types. You write these initializers as part of the protocol’s definition in exactly the same way as for normal initializers, but without curly braces or an initializer body:
protocol SomeProtocol {
init(someParameter: Int)
}
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// initializer implementation goes here
}
}
You can implement a protocol initializer requirement on a conforming class as either a designated initializer or a convenience initializer. In both cases, you must mark the initializer implementation with the required
modifier.
The use of the required
modifier ensures that you provide an explicit or inherited implementation of the initializer requirement on all subclasses of the conforming class, such that they also conform to the protocol.
NOTE: You don’t need to mark protocol initializer implementations with the required
modifier on classes that are marked with the final
modifier, because final classes can’t subclassed.
Because protocols are types, begin their names with a capital letter (such as FullyNamed
and RandomNumberGenerator
) to match the names of other types in Swift (such as Int
, String
, and Double
).
Protocols as Types
In Swift, protocols can be used as types, meaning that we can use protocols to define variables, function parameters, and return types.
When a protocol is used as a type, it represents any instance that conforms to that protocol. This allows us to write code that is more flexible and reusable because it can work with any type that conforms to the protocol, not just a specific concrete type.
For example, let’s say we have a protocol Animal
:
protocol Animal {
var name: String { get }
func speak()
}
We can define a variable of type Animal
:
var myPet: Animal
This variable can hold any instance that conforms to the Animal
protocol, regardless of the specific type of the instance. This means that we can assign instances of different types that conform to the Animal
protocol to the myPet
variable.
class Dog: Animal {
var name: String
init(name: String) {
self.name = name
}
func speak() {
print("Woof!")
}
}
class Cat: Animal {
var name: String
init(name: String) {
self.name = name
}
func speak() {
print("Meow!")
}
}
let myDog = Dog(name: "Rex")
let myCat = Cat(name: "Fluffy")
myPet = myDog // Assigns an instance of Dog to myPet
myPet.speak() // Prints "Woof!"
myPet = myCat // Assigns an instance of Cat to myPet
myPet.speak() // Prints "Meow!"
In addition to using protocols as types for variables, we can also use them as types for function parameters and return types. This allows us to write functions that can accept and return any type that conforms to a particular protocol, making the function more flexible and reusable.
For example:
func printAnimalName(_ animal: Animal) {
print(animal.name)
}
func createRandomAnimal() -> Animal {
return Bool.random() ? Dog(name: "Rex") : Cat(name: "Fluffy")
}
The printAnimalName
function takes an instance of any type that conforms to the Animal
protocol as a parameter, while the createRandomAnimal
function returns an instance of a random type that conforms to the Animal
protocol.
In summary, using protocols as types in Swift allows us to write more flexible and reusable code, as it can work with any type that conforms to the protocol, not just a specific concrete type.