Now that you have a deeper understanding about layout and user interfaces, it's time to take a step back and take a look at the underlying code we're writing to build apps. If you have experience with Object Oriented Programming (OOPs), the way we were programming so far should look familiar to you. You might not have seen protocols used in class declarations before, but apart from that, inheritance in Swift is pretty much the same as you might have seen before.
However, as you have been playing around with Swift and iOS development, you might have noticed two object types you have not seen before: structs and enums. These two object types are what we refer to as value types. What this means and how and why it matters is covered in this chapter.
You will learn what structs and enums are and how you can use them in your own applications. You will also learn how these object types compare to traditional classes and how to make a choice between using a class: enum or a struct. First, we'll look into classes and how they manifest themselves throughout apps a bit more, so you have a better understanding of the situation as you've seen it being set up until now. The topics covered in this chapter are the following:
At the end of this chapter, you'll be able to distinguish between different object types and you should be able to use them in your own apps. During this chapter, we'll use a Playground to experiment with code. You can check out the Playground for this chapter in the Git repository for this book or you can create a new Playground and follow along.
In the previous chapters, you've been creating classes to contain your app's logic and user interface. This works perfectly fine, and to make use of these classes, we didn't need to know anything about how classes behave on the inside and how they manifest themselves in terms of memory and mutability. To understand value types and how they compare to reference types, it's essential that you do have an understanding of what's going on under the hood.
In Swift, there are two types of object that are considered a reference type: classes and closures. Classes are the only objects in Swift that can inherit from other classes. More on this later when we discuss structs. Let's examine what it means for an object to be a reference type first.
Whenever you create an instance of a class, you have an object that you can use and pass around. For example, you could use an instance of a class as an argument of a method. This is demonstrated in the following snippet:
class Pet { var name: String init(name: String) { self.name = name } } func printName(forPet pet: Pet) { print(pet.name) } let cat = Pet(name: "Bubbles") printName(forPet: cat) // Bubbles
The preceding code isn't too exciting. We pass an instance of the Pet
class to the printName
function and it prints our pet's name. By passing the instance of Pet
to a function, we actually passed a reference to our Pet
instance to the function. This might sound confusing, and before everything becomes clear, let's make things a little bit more confusing. If you change the printName
function as displayed in the following snippet, we can see some behavior that probably confuses you just a bit more:
func printName(forPet pet: Pet) { print(pet.name) pet.name = "Jeff" } let cat = Pet(name: "Bubbles") printName(forPet: cat) // Bubbles print(cat.name) // Jeff
After calling the printName
function, our cat has a different name. You probably consider this strange; how does setting the name of the pet you received in the function change the name of the cat
variable? If you've programmed in other languages, you might consider this obvious; after all, didn't we just pass that exact constant to the printName
function? Also, doesn't it make sense then that the name of the original constant also changed? If this is what you thought, then your mindset is in the world of reference types. If this isn't what you thought, you expected our constant to behave more like a value type, which isn't a bad thing at all. However, you still don't really know what exactly is happening with our Pet
instance.
The snippet we used to change the Pet
's name is small, but it shows exactly how reference types work. Whenever you create an instance of a class, some space in memory is allocated for the instance. When you pass the instance as an argument to a function, you don't pass the contents of the instance. Instead, you pass the memory address or, in other words, a reference to the instance. Unless you explicitly copy a reference type, every time you pass your instance around or point another variable to it, the same address in memory is changed. Let's expand our initial example a bit more, so this becomes even more apparent:
let cat = Pet(name: "Bubbles") let dog = cat printName(forPet: cat) // Bubbles dog.name = "Benno" print(cat.name) // Benno
Here, we created a cat constant, then we created a dog
constant and set the cat
constant as its value. Then, we called the printName
function; this works as before. Next, we changed the name of the dog. When we print the name of the cat
instance, it's not what you might have expected at first because the name of the dog
is printed. That's because we didn't copy anything, we simply pointed the dog
constant to the same address in memory as the cat
pointed to.
Reference types are passed around by the address in memory they point to. Unless you explicitly copy an object, multiple variables or constants are pointing to the same instance of the object. As a result of this, mutating one of these variables will mutate them all.
Although this behavior can be convenient at certain times, it could also turn out to pack some nasty surprises if your apps grow to a substantial size. Therefore, it's a good idea to understand value types and know when to use them because value types can save you quite some debugging time.