47.
Swift Playgrounds, Classes & Structs
Written by Joey deVilla
Before we move to the next project, we should take a closer look at structs and how they differ from classes.
For most of this book, the objects that you’ve been creating have been instances of class. With the SwiftUI version of Bullseye, you may have wondered why the code that defines the layout and behaviour of SwiftUI screens were instances of struct instead. Why switch from one type of object to another?
There’s a simple one-word answer: Swift. Like a lot of Xcode’s error messages, this answer is technically correct, but vague, confusing and unsatisfying. Before you proceed with the next project, let’s look more closely at structs and classes.
You may be tempted to skip this chapter and simply jump to the next project. Please don’t — just as classes were a key part of the old way of building iOS apps, structs are a key part not just of the new way of building iOS apps, but of Swift programming in general. There’s some valuable information coming up!
Rather than walk you through a dry technical tour of structs and classes and their similarities and differences, let’s play with them using using an Xcode feature appropriately called playgrounds.
Here’s what you’ll see in this chapter:
- Playgrounds: Think of them as whiteboards where you can try out code ideas without having to build a whole project first.
- Classes: You’ve been using them for a while, but it never hurts to review what you’ve learned, and you’ll learn a little bit about inheritance while you’re at it.
- Structs: They’re another way to create objects, and when combined with protocols, they’re pretty powerful.
- When to use structs and when to use classes: There’s one that you should use more often. I’ll tell you which one, and why.
Playgrounds
A playground is a type of Xcode project that lets you experiment with Swift code and see the results immediately. Think of it as a “scratchpad” where you can try out a new idea before putting it in one of your projects, or as a way of learning about an unfamiliar keyword or feature.
Those of you who have used languages like JavaScript, Python or Ruby with a REPL (Read-Evaluate-Print Loop) where you can type in a single line of code and see immediate results will see that playgrounds are a similar form of tool, yet considerably more powerful. We’ll only scratch the surface of what playgrounds can do in this chapter.
Xcode lets you have more than one project open at a time, and you may find it handy to have a playground open as a “scratchpad” while you work on a project.
Let’s create a playground, which we’ll use to explore classes and structs.
➤ In Xcode’s File menu, select New…, and then Playground. You’ll see a pop-up where you select options for the playground you want to create.
I’ve found that the blank macOS playground is the one best suited for experimenting with Swift. That’s because it doesn’t load all the extra material that iOS and tvOS programming require, and it crashes less often.
➤ In the pop-up, select macOS, highlight the Blank playground type, then click Next. You’ll see a Save As: dialog:
➤ Enter a name for the playground (I used Structs and classes; you can use whatever you like). In the Add to: menu, select Don’t add to any project or workspace. Once you’ve done that, click the Create button.
Xcode will create a new playground, which will look like this:
You can see what all the code up to and including a particular line in the playground does by moving the cursor over its line number and pressing the “Play” button that appears. The results will appear in the live view sidebar on the right.
➤ Move the cursor over the number for line 3 — var str = "Hello, playground". A “Run” button will appear and replace the number:
➤ Click the “Run” button. Every line of code in the playground, up to and including the line for which you clicked the “Run” button will be executed.
You should see the result of line 3 — “Hello, playground” — appear in the live view sidebar:
To the right of the result of line 3, you’ll see a rectangular button. This is the “Show result” button. If you move the cursor so that it’s both within the live view sidebar and line 3, a second button shaped like an eye will appear to the left of the “Show result” button. This is the “Quick view” button:
Clicking the “Show result” button for a line of code in the playground causes the result for that line of code to be constantly displayed below the line:
Clicking the “Quick look” button for a line of code in the playground causes a pop-up containing the result for that line of code to appear. It disappears as soon as you click anywhere else on the screen:
In addition to the live view sidebar, playgrounds also have a debug console at the bottom of the screen. You can used print() statements to output text to the debug console.
Add the following line to the playground:
print("str contains: \(str)")
➤ Move the cursor over the line number of the line you just entered and click the “Run” button. You should see the output of the print() statement in the debug console at the bottom of the playground:
Now that we’ve covered the basics of playgrounds, let’s experiment with structs and classes.
Classes
Let’s review classes by creating a class that represents cats.
➤ Replace the contents of the playground with the following:
import Cocoa
class CatClass {
var name: String
var weight: Double // In kilograms, just to be
// cientific or international
// (take your pick)
init(name: String, weight: Double) {
self.name = name
self.weight = weight
}
func report() {
print("\(name) weighs \(weight) kilograms.")
}
func fatten() {
print("Fattening \(name)...")
weight += 0.5
report()
}
}
There’s a reason we gave this class the redundant name
CatClass: Later on, we’ll later create thestructequivalent of this class, and we’ll need to be able to differentiate between the two.
Let’s create a couple of instances of CatClass to represent the following cats:
- Anastasia, a cat that weighs 2.5 kilograms (about 5.5 pounds).
- Bao, a heftier cat weighing in at 6.3 kilograms.
(We’re going international not just with cat measurements, but their names as well.)
➤ Add the following line to the playground:
var classCat1 = CatClass(name: "Anastasia", weight: 2.5)
let classCat2 = CatClass(name: "Bao", weight: 6.3)
classCat1.report()
classCat2.report()
➤ Run all the code in the playground by moving the cursor over the line number of the last line of code or the line after that. Click on the “Run” icon that appears.
The debug console will contain the following lines: Anastasia weighs 2.5 kilograms. and Bao weighs 6.3 kilograms.
Constant and variable class instances
classCat1, representing the cat named Anastasia, was declared with the var keyword, which means that it’s a variable. This means that you can assign another instance of CatClass to it.
➤ Add the following line to the playground:
classCat1 = CatClass(name: "Cuddles", weight: 8.0)
classCat1.report()
➤ Run all the code in the playground by moving the cursor over the line number of the last line of code or the line after that. Click on the “Run” icon that appears.
The last line in the debug console will contain the output of classCat1’s report() method, which will be Cuddles weighs 8.0 kilograms.
As you can see, the cat referenced in classCat1 is no longer a 2.5 kilogram cat named Anastasia, but a much larger cat named Cuddles.
We’ll do the same thing with classCat2. It represents a cat named Bao and was declared with the let keyword. This makes it a constant, which means that you can’t assign it another instance of CatClass. Let’s try anyway.
➤ Add the following line to the playground:
classCat2 = CatClass(name: "Dmitry", weight: 4.7)
In short order, Xcode will let you know that you can’t re-assign a constant with the error message Cannot assign to value: ‘classCat2’ is a ‘let’ constant:
➤ Comment out the line where you tried to re-assign classCat2. Do this by either manually adding // to the start of the line, or moving the cursor to that line and pressing command + /:
//classCat2 = CatClass(name: "Dmitry", weight: 4.7)
This should reinforce the point that when you put something inside a constant, you can’t put something else inside it. That’s why it’s called a constant.
Changing properties of constant and variable class instances
Let’s try changing the properties of both our cats. We’ll start with classCat1, the variable catClass instance. We’ll change its name property directly and use the fatten() method to change its weight.
➤ Add the following lines to the playground:
classCat1.name = "Esmerelda"
classCat1.fatten()
➤ Run all the code in the playground. You’ll see from the output in the debug console that the classCat1 cat’s name is now Esmerelda, and it weighs 8.5 kilograms — half a kilogram heavier than before.
We’ll try the same thing with classCat2, the constant classCat instance.
➤ Add the following lines to the playground:
classCat2.name = "Faiza"
classCat2.fatten()
➤ Run all the code in the playground. You’ll see from the output in the debug console that the classCat2 cat’s name is now Faiza, and like the previous cat, it’s also half a kilogram heavier than before.
Wait a minute — wasn’t classCat2 a constant? Shouldn’t you be unable to change it? What’s going on here?
Classes are reference types
When you assign an instance of a class to a variable or constant, that variable or constant doesn’t contain the instance itself. Instead, it contains a reference to the instance. Think of references as being like someone’s email address — it’s not the person, but a way to reach that person.
Let’s look at what happened when you first assigned the classCat1 variable. This was the original line of code:
Here’s what this line of code does:
- The code to the right of the
=sign calls theCatClass’ initializer, which creates a new instance ofCatClasssomewhere in the device’s RAM. - The code to the left of the
=sign creates the variablecatClass1somewhere else in the device’s RAM. - The
=sign takes the location ofCatClassinstance and stores that location inclassCat1.
Later, when you assigned another class instance to classCat1, this happened:
Here’s what the code does:
- The code to the right of the
=sign calls theCatClass’ initializer, which creates a new instance ofCatClasssomewhere in the device’s RAM. - The code up to and including the left of the
=sign takes the location ofCatClassinstance and stores that location inclassCat1. - The old
CatClassinstance is no longer referenced byclassCat1. The system will eventually detect that the instance is no longer being used and will eventually delete it from RAM.
You can change the CatClass instance that classCat1 refers to because classCat1 is a variable.
Now let’s look at classCat2, which is a constant. Here’s the line of code where you declared it:
Here’s what this line of code does:
- The code to the right of the
=sign calls theCatClass’ initializer, which creates a new instance ofCatClasssomewhere in the device’s RAM. - The code to the left of the
=sign creates the constantcatClass2somewhere else in the device’s RAM. - The
=sign takes the location ofCatClassinstance and stores that location inclassCat2.
Since classCat2 is a constant, you can’t change its contents after you’ve put something in it. This means that it will always refer to the same CatClass instance. However, that instance has var properties, which you can change.
What happens when you assign a class instance variable to another class instance variable?
So far, we’ve been assigning class instances to variables and constants. What happens when you assign a class instance variable to another class instance variable? Let’s find out by creating two new class instances.
➤ Add the following lines to the playground, and then run all the code:
var classCat3 = CatClass(name: "Imelda", weight: 6.1)
var classCat4 = CatClass(name: "Jasmine", weight: 2.2)
classCat3.report()
classCat4.report()
The last two lines in the playground’s debug pane should be Imelda weighs 6.1 kilograms. and Jasmine weighs 2.2 kilograms.
Here’s a diagram showing the two variables and class instances you just created.
It should give you an idea of what happens if we were to assign classCat3’s value to classCat4. Let’s find out!
➤ Add the following lines to the playground, and then run all the code:
classCat4 = classCat3
classCat3.report()
classCat4.report()
The last two lines in the playground’s debug pane should be identical: Imelda weighs 6.1 kilograms.
You might assume that we simply copied classCat3’s instance over to classCat4. Let’s test that assumption by changing one of the properties in classCat3’s instance.
➤ Add the following lines to the playground, and then run all the code:
classCat3.name = "Kenji"
classCat3.report()
classCat4.report()
The last two lines in the playground’s debug pane should be identical: Kenji weighs 6.1 kilograms.
classCat3 and classCat4 are both references to the same class instance. Here’s what happened when you set classCat4 to classCat3’s value:
And here’s what happened when you changed the name of classCat3’s cat to “Kenji”:
Since they both reference the same class instance, any change made to the instance through catClass4 will affect what you get when you access it with catClass3.
➤ Add the following lines to the playground, and then run all the code:
classCat4.fatten()
classCat3.report()
classCat4.report()
The last two lines in the playground’s debug pane should be two lines indicating the Kenji’s put on a little weight: Kenji weighs 6.6 kilograms.
If you’ve been following all the exercises in this book, you’ve been working with classes for a while and you may have internalized how reference types behave. Still, it never hurts to review them, especially when I’m about to show you structs, which differ from classes in some very fundamental ways.
Classes and inheritance
One of the first things that many books on object-oriented programming will tell you about classes is that they can inherit from other classes. Inheritance lets you build a class using another class, creating a more specialized class in the process.
You’ve been using inheritance without realizing it throughout the UIKit projects in this book. For example, any view controller that you created in those projects inherits from the generic view controller class called UIViewController, creating a view controller that meets the specific needs of the screen you were working on.
Since we’re playing around with classes in a playground, we’ve got the perfect opportunity to look at inheritance in a more deliberate manner.
Suppose we want an class that represents a robot cat. It has all the qualities and abilities of the cat represented by the CatClass class, but has a couple of extra features: it has a laser which it can fire, and a set amount of energy it uses to power the laser.
We could build a whole new class to represent the robot cat, but this is also a chance to create the robot cat class using inheritance.
➤ Add the following lines to the playground:
class RoboCat: CatClass {
var laserEnergy: Int
init(name: String, weight: Double, laserEnergy: Int) {
self.laserEnergy = laserEnergy
super.init(name: name, weight: weight)
}
func fireLaser() {
if laserEnergy > 0 {
print("\(name) fires a laser. Pew! Pew!")
laserEnergy -= 1
} else {
print("No energy to fire laser.")
}
}
override func report() {
print("\(name) weighs \(weight) kilograms and has \(laserEnergy) units of laser energy.")
}
}
Let’s look at the code bit by bit. Here’s the first line:
class RoboCat: CatClass {
This line declares the RoboCat class, and specifies that it inherits from CatClass. In the CatClass-RoboCat relationship, CatClass is the superclass and RoboCat is the subclass.
This means that RoboCat instances have all the properties and methods defined in CatClass as well as its own properties and methods. Everything defined in the body of the RoboCat class is in addition to what CatClass provides. RoboCat defines one property…
var laserEnergy: Int
…and it inherits two properties from its superclass, CatClass, making for a total of three properties:
var name: String
var weight: Double // In kilograms, just to be
// be scientific or international
// (take your pick)
Let’s look at Robocat’s initializer:
init(name: String, weight: Double, laserEnergy: Int) {
self.laserEnergy = laserEnergy
super.init(name: name, weight: weight)
}
The initializer takes three arguments, one for each property. It sets the value of the laserEnergy property — the one unique to RoboCat — directly. It then uses the object representing the superclass, super, to call the initializer for CatClass, which instantiates the superclass and sets its properties.
To create an instance of a class that inherits from another class, you need to instantiate both classes. The initializer for a subclass must call the initializer for its superclass, usually in its final line.
The next part of RobotCat is the fireLaser() method. It’s pretty straighforward:
func fireLaser() {
if laserEnergy > 0 {
print("\(name) fires a laser. Pew! Pew!")
laserEnergy -= 1
} else {
print("No energy to fire laser.")
}
}
And finally, we have the report() method, which also exists in the superclass:
override func report() {
print("\(name) weighs \(weight) kilograms and has \(laserEnergy) units of laser energy.")
}
CatClass’ version of report() displays the cat instance’s name and weight, but it wasn’t designed with RoboCat’s laser energy in mind. RobotCat needs its own version of report() that overrides the version it inherits from its superclass.
That’s what override keyword that precedes func report() does: It tells Swift that RoboCat should use its own version of report() rather than the one defined by its superclass.
Let’s create an instance of our new class!
➤ Add the following lines to the playground:
let classCat5 = RoboCat(name: "FELINE SECURITY UNIT", weight: 20.0, laserEnergy: 10)
classCat5.fireLaser()
classCat5.fatten()
➤ Run the playground. The final lines in the debug console should be:
- FELINE SECURITY UNIT fires a laser. Pew! Pew!
- Fattening FELINE SECURITY UNIT…
- FELINE SECURITY UNIT weighs 20.5 kilograms and has 9 units of laser energy.
As you can see, the RoboCat instance has all the capabilities of a CatClass instance, plus its own robot cat capabilities.
In Swift, a class can have only one superclass. In computer science, this is called single inheritance. The designers of Swift put in this limitation (and so did the designers of a number of other languages, including C#, Java, JavaScript, and Kotlin) because experience has shown that having classes that can inherit from multiple superclasses — multiple inheritance — seems to create more problems than it solves.
Now that we’ve looked at classes, let’s look at structs.
Structs
At first glance, Structs look a lot like classes. They also function as “blueprints” that you can use to create objects or instances with properties and methods. You access a struct’s properties and methods in the same way you do so with a class.
You been working with structs for some time without knowing it. Swift has a number of built-in structs to store data, and you’ve been using them since the earliest parts of this book. Things like String, Array, and Dictionary may seem like classes, but they’re all structs. You’ve also used structs from libraries such as UIKit and Core Location: CGPoint, CGRect, and CLLocationCoordinate2D, to name a few.
SwiftUI is built on structs and all the things that go along with them. In order to understand SwiftUI, you need to understand structs.
Let’s explore structs by creating the struct equivalent of CatClass. Since we haven’t covered structs in very much detail until now, we’ll build our cat struct slowly, starting with just the properties.
➤ Add the following to the playground:
struct CatStruct {
var name: String
var weight: Double // In kilograms, just to be
// be scientific or international
// (take your pick)
}
If this were a class, it wouldn’t be complete. You’d still need to add an initializer so that the values for the name and weight properties can be set when you create an instance.
However, this is a struct, and if you don’t provide an initializer for its properties, Swift provides a default one behind the scenes.
➤ Add the following line to the playground:
var structCat1 = CatStruct(name: "Latifah", weight: 3.9)
As you type in this line, Xcode will suggest that you use the default initializer:
The default initializer has one parameter for each property, and the parameters appear in the order in which the properties are declared in the struct. Since the name property is declared before the weight property in CatStruct, its initializer’s first parameter is name:, followed by weight:.
If you like fancy-sounding terms, the proper name for the default initializer for structs is memberwise initializer.
➤ Run the playground, then click on the Show result button for the line where you declared structCat1. You should see this:
Now that we’ve given CatStruct its properties and seen that structs come with a built-in default initializer, let’s see how struct properties work.
➤ Add the following lines to the playground:
structCat1.name = "Mongo"
structCat1.weight = 10.0 // Mongo likes candy!
➤ Run the playground, then click on the Show result button for the line where you set structCat1’s weight property to 10. You should see this:
As you can see, struct properties seem to behave like class properties.
Let’s move on from properties to methods. We’ll implement the report() method first.
➤ Modify CatStruct so that it looks like this:
struct CatStruct {
var name: String
var weight: Double // In kilograms, just to be
// be scientific or international
// (take your pick)
func report() {
print("\(name) weighs \(weight) kilograms.")
}
}
➤ Add the following line to the end of the playground:
structCat1.report()
➤ Run the playground. The final line in the debug console should be Mongo weighs 10.0 kilograms.
Let’s add the fatten() method next.
➤ Modify CatStruct so that it looks like this:
struct CatStruct {
var name: String
var weight: Double // In kilograms, just to be
// be scientific or international
// (take your pick)
func report() {
print("\(name) weighs \(weight) kilograms.")
}
func fatten() {
print("Fattening \(name)...")
weight += 0.5
report()
}
}
A moment after you make the change, Xcode will complain:
Let’s explore what’s happening here.
Mutating struct functions
Let’s first make sure that we haven’t gone crazy. Comment out CatStruct’s fatten() method for now.
➤ Modify CatStruct so that it looks like this:
struct CatStruct {
var name: String
var weight: Double // In kilograms, just to be
// be scientific or international
// (take your pick)
func report() {
print("\(name) weighs \(weight) kilograms.")
}
// func fatten() {
// print("Fattening \(name)...")
// weight += 0.5
// report()
// }
}
Now let’s try fattening Mongo (not that he needs it) from outside the struct.
➤ Add these lines to the playground:
structCat1.weight += 0.5
structCat1.report()
➤ Run the playground. The final line in the debug console should be Mongo weighs 10.5 kilograms.
The line that increases the cat’s weight by half a kilogram, weight += 0.5, works just fine when you use it outside the struct, but produces one of Xcode’s classic cryptic error messages when used inside the struct.
This is the way struct properties were designed to work. By default, only code outside a struct can change its properties. However, there are a couple of ways to go around this rule:
- If the struct is a SwiftUI view (or more technically, if it adopts or conforms to the SwiftUI
Viewprotocol), you can allow the struct to change a property by making it a state property with the@Stateattribute. You’ve already seen this action while building the SwiftUI version of Bullseye. - You can mark any method in a struct with the
mutatingkeyword, which allows code within that method to change the struct’s properties. You’ve also seen this in action, when you created an extension forString(remember,Stringis a struct) while working on the My Locations app.
Let’s make fatten() work by marking it with the mutating keyword.
➤ Uncomment CatStruct’s fatten() method and add the mutating keyword so that it looks like this:
mutating func fatten() {
print("Fattening \(name)...")
weight += 0.5
report()
}
➤ Add this line to the end of the playground:
structCat1.fatten()
➤ Run the playground. The final line in the debug console should be Mongo weighs 11.0 kilograms.
Structs are value types
One really big difference between structs and classes is that while classes are reference types, structs are value types. When you assign a struct instance to a variable, you’re putting the struct into that variable, not a reference to the struct.
Let’s explore this difference by creating another struct.
➤ Add these lines to the end of the playground:
var structCat2 = structCat1
structCat2.report()
structCat2.name = "Naveen"
structCat2.weight = 5.3
structCat1.report()
structCat2.report()
➤ Run the playground. The last two lines in the debug pane will be Mongo weighs 11.0 kilograms. and Naveen weighs 5.3 kilograms.
The line var structCat2 = structCat1 creates a new variable named structCat2 and copies the contents of structCat1 into it. Since structCat1 contains a struct instance for the cat named Mongo, structCat2 is filled with a copy of that struct instance:
Since structCat1 and structCat2 each contain their own CatStruct instance, changing structCat2’s properties affects only the instance contained in structCat2:
You’ve probably already figured this next one out, but let’s go through the exercise just to be thorough: Can you modify the properties of a constant struct instance?
➤ Add these lines to the playground:
let structCat3 = CatStruct(name: "Orson", weight: 5.5)
structCat3.fatten()
In short order, Xcode will point out that you can’t do that:
That makes sense. struct3 is a constant, which means you can’t change its contents, and changing any of the properties of the instance within is changing its contents.
➤ Comment out the last two lines of the playground so that they look like this:
//let structCat3 = CatStruct(name: "Orson", weight: 5.5)
//structCat3.fatten()
Structs and protocols
Structs don’t support inheritance. They do support protocols (and so do classes and enums), which were described earlier in this book as being like job ads, in that they list the things that a candidate for a certain job should be able to do, but don’t specify how the candidate will perform those tasks.
Earlier in the book, you used protocols to list the tasks that you passed to a delegate. Let’s now look at how protocols can be used to build structs that represent regular cats and laser-equipped robot cats.
➤ Add the following to the playground:
protocol Pet {
var name: String { get set }
var weight: Double { get set }
func report() -> ()
mutating func fatten() -> ()
}
The Pet protocol defines the properties that a pet should have. Let’s take a closer look at them:
var name: String { get set }
var weight: Double { get set }
This bit of code looks mostly like the properties in a struct or class, except that they’re followed by { get set }. This says that the struct, class, or enum that uses the protocol must allow the property to be read (get) and changed (set). If a property in a protocol should be reabable but not changeable, it should be followed by { get }. In the considerably more rare case where a property in a protocol should be changeable but not readable, it should be followed by { set }.
The Pet protocol also defines the methods that a pet should have:
func report() -> ()
mutating func fatten() -> ()
Notice that the protocol only specifies the following about its methods:
- The name for each method.
- The type of each method. Both methods’ types are
() -> (), which means that they doesn’t have any parameters, and they don’t return any values. - If the method is
mutating.
The protocol says nothing about the code inside any of its methods. That’s because it’s up to the struct, class, or enum that adopts the protocol.
Let’s put the protocol to use by defining a new struct to represent ordinary cats.
➤ Add the following to the playground:
struct ProtocolCat: Pet {
var name: String
var weight: Double
func report() {
print("\(name) weighs \(weight) kilograms.")
}
mutating func fatten() {
print("Fattening \(name)...")
weight += 0.5
report()
}
}
➤ Add the following lines to the playground, then run the playground:
var structCat4 = ProtocolCat(name: "Pasquale", weight: 7.7)
structCat4.report()
structCat4.fatten()
The last lines in the debug pane will be Pasquale weighs 7.7 kilograms., Fattening Pasquale…, and Pasquale weighs 8.2 kilograms.
Protocols become more powerful once you pair them with extensions. You’ve used them to add extra code to an existing class and struct, and you can also use them to add code to protocols.
Let’s suppose you’re quite sure that all pets should have the same code in their report() and fatten() methods. You could use an extension to specify the code for these methods.
➤ Replace ProtocolCat with the following:
struct ProtocolCat: Pet {
var name: String
var weight: Double
}
extension Pet {
func report() {
print("\(name) weighs \(weight) kilograms.")
}
mutating func fatten() {
print("Fattening \(name)...")
weight += 0.5
report()
}
}
You’ve just done two things:
- You reduced
ProtocolCatto just its properties. - You used an extension to add the code for the
report()andfatten()methods to thePetprotocol. This means that objects that adopt thePetprotocol no longer have to provide the code for those methods.
➤ Run the playground. The last lines in the debug pane will be Pasquale weighs 7.7 kilograms., Fattening Pasquale…, and Pasquale weighs 8.2 kilograms.
The protocol makes it easy to create new kinds of pets. Let’s create a struct that represents dogs that has all the capabilities provided by Pet, plus one extra thing: fetch().
➤ Add the following to the playground:
struct Dog: Pet {
var name: String
var weight: Double
func fetch() {
print("You throw a ball, and \(name) gets it and brings it back to you.")
}
}
Since Dog adopts the Pet protocol and since the Pet protocol has an extension that provides implementations of the methods listed in the protocol (report() and fatten()), we don’t have to implement them in Dog. The only method we have to implement is the one unique to Dog: fetch().
Let’s take Dog for a trial run.
➤ Add the following to the playground:
var myDog = Dog(name: "Quincy", weight: 9.4)
myDog.report()
myDog.fatten()
myDog.fetch()
➤ Run the playground. The last lines in the debug pane will be Quincy weighs 9.4 kilograms., Fattening Quincy…, Quincy weighs 9.9 kilograms., and You throw a ball, and Quincy gets it and brings it back to you.
We can take a similar approach to building robot cat instances. We can first build a protocol for laser-equipped creatures, as well an extension that provides implementations for the protocol’s methods.
➤ Add the following to the playground:
protocol LaserEquipped {
var laserEnergy: Int { get set }
mutating func fireLaser() -> ()
}
extension LaserEquipped {
mutating func fireLaser() {
if laserEnergy > 0 {
print("Firing laser. Pew! Pew!")
laserEnergy -= 1
} else {
print("No energy to fire laser.")
}
}
}
➤ With the LaserEquipped protocol and extension defined, we can create robotic cat and dog structs. Since the name RoboCat is taken by a class, we’ll call this cat struct LaserCat, and following that pattern, we’ll call the dog struct LaserDog.
➤ Add the following to the playground, then run the playground:
struct LaserCat: Pet, LaserEquipped {
var name: String
var weight: Double
var laserEnergy: Int
}
var laserKitty = LaserCat(name: "Renoir", weight: 20.0, laserEnergy: 20)
laserKitty.report()
laserKitty.fatten()
laserKitty.fireLaser()
struct LaserDog: Pet, LaserEquipped {
var name: String
var weight: Double
var laserEnergy: Int
func fetch() {
print("You throw a ball, and \(name) gets it and brings it back to you.")
}
}
var laserPuppy = LaserDog(name: "Salieri", weight: 20.0, laserEnergy: 20)
laserPuppy.report()
laserPuppy.fatten()
laserPuppy.fireLaser()
laserPuppy.fetch()
These new structs combine the capabilities provided by both the Pet and LaserEquipped protocols. That’s the power of protocols — they can be used as building blocks for sophisticated objects, and that’s how SwiftUI uses them.
One more thing: Can you override protocol methods in the same the way you can override inherited class methods? I’ll provide the answer by example.
➤ Add the following to the playground, then run the playground:
struct Hamster: Pet {
var name: String
var weight: Double
var isOnHamsterWheel: Bool
func report() {
let wheelStatus = isOnHamsterWheel ? "on" : "not on"
print("\(name) weighs \(weight) kilograms, and is \(wheelStatus) its hamster wheel.")
}
}
var myHamster = Hamster(name: "Tetsuo", weight: 0.1, isOnHamsterWheel: true)
myHamster.report()
When you run the playground, the final line in the debug pane should be Tetsuo weighs 0.1 kilograms, and is on its hamster wheel.
The Hamster struct adopts the Pet protocol, whose extension provides an implementation of report(). Unfortunately, that implementation doesn’t specify if the hamster is on its hamster wheel or not. To get around that problem, we added a hamster-specific version of report() in Hamster, which overrides the one provided by Pet, giving us complete information about the hamster.
There’s a lot more to structs and protocols, but this should be enough information to get you started — and hey, we still have one more app to write!
When to use structs and when to use classes
Swift has opinions
Sooner or later, when you’re searching the web for information about programming, you’re going to see programming languages and frameworks being described as “opinionated”. It’s actually the creators and maintainers who have opinions about the way programmers should do things, and they’ve designed their languages and frameworks so that there are “right” or “approved” ways of doing things when using them. Programming languages vary in how opinionated they are, and Swift is on the more opinionated side of the spectrum.
One Swift opinion is that you should use constants by default and variables only when necessary. It turns out that many values that we put into variables never change, so they might as well be put into constants. The use of constants also prevents a lot of errors that come up because the programmer was surprised by a value that they didn’t expect to change. Swift enforces this opinion by using the let keyword to declare constants instead of the const keyword that most other languages use. By using a keyword that’s as short as its counterpart for variables — var — Swift makes it easier to use constants by default, which in the opinion of Swift’s creators is the right way to program.
Another Swift opinion is that parameter names should be part of a method call. Where other languages would have you call methods this way…
superKitty = LaserCat("Uma", 20.0, 20)
…Swift enforces the opinion that you should include the parameter names in a method call so that it’s easy to see what each value represents:
superKitty = LaserCat(name: "Uma", weight: 20.0, laserEnergy: 20)
Swift does this by making it so that method calls that include parameter names is the default. You can create methods that don’t require parameter names when called, but Swift makes you do extra work to get them.
Swift’s opinion: Use structs by default
Just as Swift’s (or more accurately, its creators and maintainers) opinion is that you should use constants by default and variables only when necessary, it’s also the prevailing opinion that you should use structs by default and classes only when necessary.
You’ll see this opinion in a lot of places in Swift. While many other programming languages use classes for fundamental data types like strings, arrays, and dictionaries, Swift implements them as structs. Newer Apple frameworks, such SwiftUI, use objects based on structs rather than classes.
Structs are easier to reason about
The fact that structs are value types and not reference types makes it easier to think about their logic, which makes them less error-prone as a result. Consider the case where you want to see if two string values are equal. This is easy in Swift, where strings are structs, and therefore value types:
if string1 == string2 {
// The rest of the code goes here
}
In languages where strings are classes and therefore reference types, the comparison string1 == string2 doesn’t compare the values of the two strings, but whether or not string1 and string2 refer to the same string object. These languages end up having to use workarounds that look like this:
if string1.equals(string2) {
// The rest of the code goes here
}
Using structs means that there are fewer surprises. When a number of things can reference the same object, it’s possible for one of those things to alter the object in a way that you didn’t expect. For those of you who’ve lived with housemates, think of the time when one of them ate something from the refrigerator that you’d been saving for yourself.
Another Swift opinion: Favor protocol-oriented programming over object-oriented programming
In the playground exercise, you built created a laser-equipped robot cat class, RoboCat, by subclassing — that is, inheriting from — the Cat class. You could also make a flying cat class that has properties and methods related to flying by subclassing the Cat class.
But what happens when you want to create a flying robot cat? Swift’s object-oriented model is limited to single inheritance — a class can have only one superclass. You could design the robot cat class to subclass the cat class, and design the flying cat class to subclass the robot cat class, but you’re now committed to a specific inheritance order. You can’t create flying cats that aren’t also robot cats.
That’s the problem with traditional object-oriented programming: you end up with inheritance hierarchies that are inflexible and often require you to either be a psychic who can predict the way you’ll use your classes in the future or come up with work-arounds for needs you couldn’t anticipate.
On the other hand, our playground exercise demonstrated the power of protocols. They allow us to build objects out of specific functionalities. In our simple playground exercise, the LaserEquipped protocol wasn’t limited to cats, and it’s not even limited to pets or any other specific kind of object — any object that implements the protocol can fire a laser. Creating objects by using protocols as building blocks that can be used in any combination is more flexible than creating objects by inheriting from superclasses in a specific order.
One of the best ways to learn more about protocol-oriented programming is to watch the video of the 2015 Apple WWDC (World Wide Developers Conference) session titled Protocol-Oriented Programming in Swift, located here: https://developer.apple.com/videos/play/wwdc2015/408/
Structs provide other benefits
As value types, you put struct instance directly into a constant or variable. There’s a little more work involved in storing a class instance; the constant or variable holding it doesn’t directly store the instance, but the location in memory where the instance can be found. This level of indirection means that the system has to do extra work when accessing class instances. Working with large numbers of class instances can be slower the doing the same work with struct instances — in some cases, many orders of magnitude slower.
Reference types are often the cause of memory leaks, which is where a program keeps consuming addition RAM until it takes over and eventually crashes the system. Using structs, which are value types, makes memory leaks less likely.
Once you get into concurrent programming — that’s where you’ve got different code running at the same time — you’ll find that working with value types like structs prevents a lot of surprises. With reference types and concurrent programming, you run the risk of the “housemate problem” again, where two or more simultaneously-running blocks of code are accessing and altering the same object, producing unexpected results.
So when should you use classes?
Use classes when you need the capabilities that only a class can provide. Here are some of the more likely cases where you’ll need to use classes instead of structs:
- When you need to be able to access an object from two or more different places. Suppose you have an object that gets information from a web service (we’ll call it the “web service object“), and the information it provides is used by many other objects in your app. You might want to make the web service object a class instance. Any object that needs information from the web service would have its own reference to the web service object.
- When you need to inherit the capabilities of another class. Generally, you should avoid class inheritance, but there are times when you can’t avoid it. In most cases, this happens when you’re using a framework that was built with inheritance in mind, such as UIKit.
- When you need to interoperate with Objective-C code. Objective-C’s only object types are class instances, so if you need to pass objects to or get objects from Objective-C code, you’ll need to use classes.
That’s a lot of theory. It’s time to move on to the next app — Checklist, the SwiftUI version of the Checklists app!
You can find the file for the complete playground for this chapter under 47 - Swift Playgrounds Classes and Structs in the Source Code folder.