Structs
The most basic way that we can group together data and functionality into a logical unit or object is to define something called a structure. Essentially, a structure is a named collection of data and functions. Actually, we have already seen several different structures because all of the types such as string, array, and dictionary that we have seen so far are structures. Now we will learn how to create our own.
Types versus instances
Let's jump straight into defining our first structure to represent a contact:
struct Contact { var firstName: String = "First" var lastName: String = "Last" }
Here we have created a structure by using the struct keyword followed by a name and curly brackets ({}) with code inside them. Just like with a function, everything about a structure is defined inside its curly brackets. However, code in a structure is not run directly, it is all part of defining what the structure is. Think of a structure as a specification for future behavior instead of code to be run, in the same way that blueprints are the specification for building a house.
Here, we have defined two variables for the first and last name. This code does not create any actual variables nor does it remember any data. As with a function, this code is not truly used until another piece of code uses it. Just like with a string, we have to define a new variable or constant of this type. However, in the past we have always used literals like Sarah or 10. With our own structures, we will have to initialize our own instances, which is just like building a house based on the specifications.
An instance is a specific incarnation of a type. This could be when we create a String variable and assign it the value Sarah. We have created an instance of a String variable that has the value Sarah. The string itself is not a piece of data; it simply defines the nature of instances of String that actually contain data.
Initializing is the formal name for creating a new instance. We initialize a new Contact like this:
let someone = Contact()
You may have noticed that this looks a lot like calling a function and that is because it is very similar. Every type must have at least one special function called an initializer. As the name implies, this is a function that initializes a new instance of the type. All initializers are named after their type and they may or may not have parameters, just like a function. In our case, we have not provided any parameters so the first and last names will be left with the default values that we provided in our specification: First and Last.
You can see this in a playground by clicking on the plus sign next to Contact to the right of that line. This inserts a result pane after the line where it displays the value of firstName and lastName. We have just initialized our first custom type!
If we define a second contact structure that does not provide default values, it changes how we call the initializer. Since there are no default values, we must provide the values when initializing it:
struct Contact2 { var firstName: String var lastName: String } let someone2 = Contact2(firstName: "Sarah", lastName: "Smith")
Again, this looks just like calling a function that happens to be named after the type that we defined. Now, someone2 is an instance of Contact2 with firstName equal to Sarah and lastName equal to Smith.
Properties
The two variables, firstName and lastName, are called member variables and, if we change them to be constants, they are then called member constants. This is because they are pieces of information associated with a specific instance of the type. You can access member constants and variables on any instance of a structure:
print("\(someone.firstName) \(someone.lastName)")
This is in contrast to a static constant. We could add a static constant to our type by adding the following line to its definition:
struct Contact { static let UnitedStatesPhonePrefix = "+1" // "First Last" }
Note the static keyword before the constant declaration. A static constant is accessed directly from the type and is independent of any instance:
print(Contact.UnitedStatesPhonePrefix) // "+1"
Note that we will be adding code to existing code every so often like this. If you are following along in a playground, you should have added the static let line to the existing Contact structure.
Member and static constants and variables all fall under the category of properties. A property is simply a piece of information associated with an instance or a type. This helps reinforce the idea that every type is an object. A ball, for example, is an object that has many properties including its radius, color, and elasticity. We can represent a ball in code in an object-oriented way by creating a ball structure that has each of those properties:
struct Ball { var radius: Double var color: String var elasticity: Double }
Note that this Ball type does not define default values for its properties. If default values are not provided in the declaration, they are required when initializing an instance of the type. This means that an empty initializer is not available for that type. If you try to use one, you will get an error:
Ball() // Missing argument for parameter 'radius' in call
Just like with normal variables and constants, all properties must have a value once initialized.
Member and static methods
Just as you can define constants and variables within a structure, you can also define member and static functions. These functions are referred to as methods to distinguish them from global functions that are not associated with any type. You declare member methods in a similar way to functions but you do so inside the type declaration, as shown:
struct Contact { var firstName: String = "First" var lastName: String = "Last" func printFullName() { print("\(self.firstName) \(self.lastName)") } }
Member methods always act on a specific instance of the type they are defined in. To access that instance within the method, you use the self keyword. Self acts in a similar way to any other variable in that you can access properties and methods on it. The preceding code prints out the firstName and lastName properties. You call this method in the same way we called methods on any other type:
someone.printFullName()
Within a normal structure method, self is constant, which means you can't modify any of its properties. If you tried, you would get an error like this:
struct Ball { var radius: Double var color: String var elasticity: Double func growByAmount(amount: Double) { // Error: Left side of mutating operator // isn't mutable: 'self' is immutable self.radius += amount } }
In order for a method to modify self, it must be declared as a mutating method using the mutating keyword:
mutating func growByAmount(amount: Double) { self.radius += amount }
We can define static properties that apply to the type itself but we can also define static methods that operate on the type by using the static keyword. We can add a static method to our Contact structure that prints the available phone prefixes, as shown here:
struct Contact { static let UnitedStatesPhonePrefix = "+1" static func printAvailablePhonePrefixes() { print(self.UnitedStatesPhonePrefix) } } Contact.printAvailablePhonePrefixes() // "+1"
In a static method, self refers to the type instead of an instance of the type. In the preceding code, we have used the UnitedStatesPhonePrefix static property through self instead of writing out the type name.
In both static and instance methods, Swift allows you to access properties without using self, for brevity. self is simply implied:
func printFullName() { print("\(firstName) \(lastName)") } static func printAvailablePhonePrefixes() { print(UnitedStatesPhonePrefix) }
However, if you create a variable in the method with the same name, you will have to use self to distinguish which one you want:
func printFirstName() { let firstName = "Fake" print("\(self.firstName) \(firstName)") // "First Fake" }
I recommend avoiding this feature of Swift. I want to make you aware of it so you are not confused when looking at other people's code but I feel that always using self greatly increases the readability of your code. self makes it instantly clear that the variable is attached to the instance instead of only defined in the function. You could also create bugs if you add code that creates a variable that hides a member variable. For example, you would create a bug if you introduced the firstName variable to the printFullName method in the preceding code without realizing you were using firstName to access the member variable later in the code. Instead of accessing the member variable, the later code would start to only access the local variable.
Computed properties
So far, it seems that properties are used to store information and methods are used to perform calculations. While this is generally true, Swift has a feature called computed properties. These are properties that are calculated every time they are accessed. To do this, you define a property and then provide a method called a getter that returns the calculated value, as shown:
struct Ball { var radius: Double var diameter: Double { get { return self.radius * 2 } } } var ball = Ball(radius: 2) print(ball.diameter) // 4.0
This is a great way to avoid storing data that could potentially conflict with other data. If, instead, diameter were just another property, it would be possible for it to be different to the radius. Every time you changed the radius you would have to remember to change the diameter. Using a computed property eliminates this concern.
You can even provide a second function called a setter that allows you to assign a value to this property like normal properties:
var diameter: Double { get { return self.radius * 2 } set { self.radius = diameter / 2 } } var ball = Ball(radius: 2) ball.diameter = 16 print(ball.radius) // 8.0
If you provide a setter then you must also explicitly provide a getter. If you don't, Swift allows you to leave out the get syntax:
var volume: Double { return self.radius * self.radius * self.radius * 4/3 * 4.13 }
This provides a nice concise way of defining read-only computed properties.
Reacting to property changes
It is pretty common to need to perform an action whenever a property is changed. One way to achieve this is to define a computed property with a setter that performs the necessary action. However, Swift provides a better way of doing this. You can define a willSet function or a didSet function on any stored property. WillSet is called just before the property is changed and it is provided with a variable newValue. didSet is called just after the property is changed and it is provided with a variable oldValue, as you can see here:
var radius: Double { willSet { print("changing from \(self.radius) to \(newValue)") } didSet { print("changed from \(oldValue) to \(self.radius)") } }
Be careful to avoid creating an infinite loop when using didSet and willSet with multiple properties. For example, if you tried to use this technique to keep diameter and radius synchronized instead of using a computed property, it would look like this:
struct Ball { var radius: Double { didSet { self.diameter = self.radius * 2 } } var diameter: Double { didSet { self.radius = self.diameter / 2 } } }
In this scenario, if you set the radius, it triggers a change on the diameter which triggers another change on the radius and that then continues on forever.
Subscripts
You may also have realized that there is another way that we have interacted with a structure in the past. We have used square brackets ([]) with both arrays and dictionaries to access elements. These are called subscripts and we can use them on our custom types as well. The syntax for them is similar to the computed properties that we saw before except that you define it more like a method with parameters and a return type, as you can see here:
struct MovieAssignment { var movies: [String:String] subscript(invitee: String) -> String? { get { return self.movies[invitee] } set { self.movies[invitee] = newValue } } }
You declare the arguments you want to use as the parameters to the subscript method in the square brackets. The return type for the subscript function is the type that will be returned when used to access a value. It is also the type for any value you assign to the subscript:
var assignment = MovieAssignment(movies: [:]) assignment["Sarah"] = "Modern Family" print(assignment["Sarah"]) // "Modern Family"
You may have noticed a question mark (?) in the return type. This is called an optional and we will discuss this more in the next chapter. For now, you only need to know that this is the type that is returned when accessing a dictionary by key because a value does not exist for every possible key.
Just like with computed properties, you can define a subscript as read-only without using the get syntax:
struct MovieAssignment { var movies: [String:String] subscript(invitee: String) -> String? { return self.movies[invitee] } }
subscript can have as many arguments as you want if you add additional parameters to the subscript declaration. You would then separate each parameter with a comma in the square brackets when using the subscript, as shown:
struct MovieAssignment { subscript(param1: String, param2: Int) -> Int { return 0 } } print(assignment["Sarah", 2])
Subscripts are a good way to shorten your code but you should always be careful to avoid sacrificing clarity for brevity. Writing clear code is a balance between being too wordy and not wordy enough. If your code is too short, it will be hard to understand because meanings will become ambiguous. It is much better to have a method called movieForInvitee: rather than using a subscript. However, if all of your code is too long, there will be too much noise around and you will lose clarity in that way. Use subscripts sparingly and only when they would appear intuitive to another programmer based on the type of structure you are creating.
Custom initialization
If you are not satisfied with the default initializers provided to you, you can define your own. This is done using the init keyword, as shown:
init(contact: Contact) { self.firstName = contact.firstName self.lastName = contact.lastName }
Just like with a method, an initializer can take any number of parameters including none at all. However, initializers have other restrictions. One rule is that every member variable and constant must have a value by the end of the initializer. If we were to omit a value for lastName in our initializer, we would get an error like this:
struct Contact4 { var firstName: String var lastName: String init(contact: Contact4) { self.firstName = contact.firstName }// Error: Return from initializer without // initializing all stored properties }
Note that this code did not provide default values for firstName and lastName. If we add that back, we no longer get an error because a value is then provided:
struct Contact4 { var firstName: String var lastName: String = "Last" init(contact: Contact4) { self.firstName = contact.firstName } }
Once you provide your own initializer, Swift no longer provides any default initializers. In the preceding example, Contact can no longer be initialized with the firstName and lastName parameters. If we want both, we have to add our own version of that initializer, as shown:
struct Contact3 { var firstName: String var lastName: String init(contact: Contact3) { self.firstName = contact.firstName self.lastName = contact.lastName } init(firstName: String, lastName: String) { self.firstName = firstName self.lastName = lastName } } var sarah = Contact3(firstName: "Sarah", lastName: "Smith") var sarahCopy = Contact3(contact: sarah) var other = Contact3(firstName: "First", lastName: "Last")
Another option for setting up the initial values in an initializer is to call a different initializer:
init(contact: Contact4) { self.init( firstName: sarah.firstName, lastName: sarah.lastName ) }
This is a great tool for reducing duplicate code in multiple initializers. However, when using this, there is an extra rule that you must follow. You cannot access self before calling the other initializer:
init(contact: Contact4) { self.print() // Use of 'self' in delegating initializer // before self.init is called self.init( firstName: contact.firstName, lastName: contact.lastName ) }
This is a great example of why the requirement exists. If we were to call print before calling the other initializer, firstName and lastName would not have a value. What would be printed in that case? Instead, you can only access self after calling the other initializer, like this:
init(contact: Contact4) { self.init( firstName: contact.firstName, lastName: contact.lastName ) self.print() }
This guarantees that all the properties have a valid value before any method is called.
You may have noticed that initializers follow a different pattern for parameter naming. By default, initializers require a label for all parameters. However, remember that this is only the default behavior. You can change the behavior by either providing an internal and external name or by using an underscore (_) as the external name.
Structures are an incredibly powerful tool in programming. They are an important way that we, as programmers, can abstract away more complicated concepts. As we discussed in Chapter 2, Building Blocks – Variables, Collections, and Flow Control, this is the way we get better at using computers. Other people can provide these abstractions to us for concepts that we don't understand yet or in circumstances where it isn't worth our time to start from scratch. We can also use these abstractions for ourselves so that we can better understand the high-level logic going on in our app. This will greatly increase the reliability of our code. Structures make our code more understandable both for other people and for ourselves in the future.
However, structures are limited in one important way, they don't provide a good way to express parent-child relationships between types. For example, a dog and a cat are both animals and share a lot of properties and actions. It would be great if we only had to implement the common attributes once. We could then split those types into different species. For this, Swift has a different system of types called classes.