Before we go on, here are some tips for users of other programming languages this year:
Previously, we learned how to group related data and behavior into objects. Now we’re taking another step forward by exploring composition using Swift protocols. Protocols in Swift define a blueprint for methods, properties, and other requirements. We can mix and match functionality from different protocols to build more flexible and modular types.
If we think of struct
s as a blueprint for an object, a protocol is a blueprint for a struct
.
By conforming to a protocol, a type promises to implement the protocol's requirements. This allows you to create small, reusable pieces of behavior that can be composed together, making your code more robust and easier to maintain.
Let's extend our library catalogue system using built-in Swift protocols. In this example, we’ll use protocols like CustomStringConvertible
and Equatable
to ensure that every library item can be easily described and compared. This approach lets our catalogue handle different kinds of library materials in a uniform way.
The CustomStringConvertible
protocol allows you to print an object (i.e. 'print(book)`) showing a well-formatted output. Without this, printing an object shows the internal Swift representation.
To conform to CustomStringConvertible
, you must add a computed String
variable called description
.
Example: a struct
that conforms to CustomStringConvertible
.
struct Student : CustomStringConvertible {
let id: Int
var name: String
var age: Int
// Conformation to CustomStringConvertible.
var description: String {
return "\(name), \(age) years old."
}
}
let anahera = Student(id: 1234, name: "Anahera", age: 16)
print(anahera) // Anahera, 16 years old.
The Equatable
protocol allows you to compare two objects of the same type. Without this, you would need to compare the objects' attributes individually.
To conform to Equatable
, you must add a static method called ==
.
Example: extending the struct
above to conform to Equatable
, comparing the ID, name, and age.
struct Student : CustomStringConvertible, Equatable {
let id: Int
var name: String
var age: Int
var description: String {
return "\(name), \(age) years old."
}
// Conformation to Equatable.
static func ==(lhs: Student, rhs: Student) -> Bool {
return lhs.id == rhs.id
}
}
let anahera = Student(id: 1234, name: "Anahera", age: 16)
let jane = Student(id: 1235, name: "Jane", age: 17)
let another_anahera = Student(id: 123, name: "Anahera", age: 16)
print(anahera == jane) // false
print(anahera == another_anahera) // true
We can also create our own protocols. Protocols can, themselves, also conform to protocols!
Let's keep our protocols simple. For now, they will only define what data that any conforming struct
s should contain. To do this, they are defined as variables. Each variable has a name, a type, and a statement of mutability — whether that attribute can be modified or not.
Data that should be immutable (in other words, a constant) is defined with { get }
(i.e. you can only 'get' this information). On the other hand, if the data should be mutable (a variable), it is defined with { get set }
.
Example: a protocol to describe the basic characteristics of a person. Student
will conform to Person
rather than CustomStringConvertible
and Equatable
directly.
protocol Person: CustomStringConvertible, Equatable {
var id: Int { get }
var name: String { get set }
var age: Int { get set }
}
struct Student : Person { ... }
Now, we adjust our Student
struct to conform to the Person
protocol. Notice how we also add extra properties such as their course codes and their dean — these are not a part of the protocol, they are part of the struct
.
struct Student : Person {
let id: Int
var name: String
var age: Int
var courseCodes: [String]
var dean: String?
...
}
The Person
protocol currently only has one conformant object. The beauty of composition is that multiple objects can conform to the same protocol and thus be treated similarly by the rest of the program.
Let's create a second conformant struct called Teacher
.
struct Teacher : Person {
// These are required by the Person protocol.
let id: Int
var name: String
var age: Int
// This is additional information.
var courseCodes: [String]
// Three letter code for the teacher.
// Since the cypher might change (i.e. when a teacher marries),
// this is separate from their ID.
var cypher: String
// Mr, Ms, Miss, Mrs, Mx, Matua, Whaea, Kōkā
var salutation: String
var description: String {
// Don't show the age, it's not necessary for teachers.
return "\(salutation) \(name) (\(cypher))"
}
static func ==(lhs: Teacher, rhs: Teacher) -> Bool {
return lhs.id == rhs.id
}
}
We will create a sort of 'catalogue' of people who work at a school. It will be generic and accept any object that conforms to Person
; that means it will hold both Student
and Teacher
objects. Let's call the catalogue RAMAK
.
/// A catalogue for people.
struct RAMAK {
var people: [any Person]
/// Add a new person to the catalogue.
/// This uses a feature called generics. Check the glossary for details.
mutating func add<T: Person>(_ newPerson: T) {
// Ensure no duplicate uniqueID is added.
guard !(people.contains { person in (person as? T) == newPerson }) else {
print("Person with ID \(newPerson.id) already exists!")
return
}
people.append(newPerson)
}
/// Search people by name.
func search(byName name: String) -> [any Person] {
return people.filter { $0.name.lowercased() == name.lowercased() }
}
}
As you can see, the people
attribute, the newPerson
parameter in the add(_:)
method, and the byName name
parameter in the search(byName:)
method have a new word in their type hint: any
.
This is different from the type Any
(with a capital A) which specifies any type at all. On the other hand, any Person
means that any type that conforms to the Person
protocol can be used in these places.
Create a program that allows you to add and look up details of library items using composition with Swift protocols.
To do this:
CatalogueItem
to define a common interface for all library items. It should have the same information as now, minus the ISBNPrintItem
, conforming to CatalogueItem
, that includes the ISBNMediaItem
, conforming to CatalogueItem
, that includes properties such as format (String
) and runtime (Double
)Magazine
, DVD
, and Audiobook
that conform to 'PrintItemor
MediaItem` as appropriate
Audiobook
should conform to both PrintedItem
and MediaItem
as audiobooks have ISBN numbersbooks
variable to items
, and change the type from [Book]
to [CatalogueItem]