11.
Structures
Written by Ehab Amer
You’ve covered some fundamental building blocks of Swift. With variables, conditionals, strings, functions and collections, you’re ready to conquer the world! Well, almost.
Most programs that perform complex tasks benefit from higher levels of abstraction. In addition to an Int, String or Array, most programs use new types specific to the domain of the task at hand. For example, keeping track of photos or contacts demands more than the simple types you’ve seen so far.
This chapter introduces the first named type–structures. Structures are types that can store named properties and define actions and behaviors. Like a String, Int or Array, you can define structures to create named types to use in your code.
By the end of this chapter, you’ll know how to define and use your own structures.
You’ll begin your adventure into custom types with pizza.
Introducing Structures
Imagine you live in a town called Pizzaville. As you might expect, Pizzaville is known for its amazing pizza. You own the most popular (and fastest!) pizza delivery restaurant in Pizzaville — “Swift Pizza”.
As the owner of a single restaurant, you have a limited delivery area. You want to write a program that calculates if a potential customer is within range for your delivery drivers. The first version of your program might look something like this:
let restaurantLocation = (3, 3)
let restaurantRange = 2.5
// Pythagorean Theorem 📐🎓
func distance(
from source: (x: Int, y: Int),
to target: (x: Int, y: Int)
) -> Double {
let distanceX = Double(source.x - target.x)
let distanceY = Double(source.y - target.y)
return (distanceX * distanceX +
distanceY * distanceY).squareRoot()
}
func isInDeliveryRange(location: (x: Int, y: Int)) -> Bool {
let deliveryDistance = distance(from: location,
to: restaurantLocation)
return deliveryDistance < restaurantRange
}
isInDeliveryRange(location: (x: 5, y: 5)) // false
Simple enough, right? distance(from:to:) will calculate how far away you are from your pizza. isInDeliveryRange(location:) will return true only if you’re not too far away.
A successful pizza delivery business may eventually expand to include multiple locations, adding a minor twist to the deliverable calculator.
Replace your existing code with the following:
let restaurantLocation = (3, 3)
let restaurantRange = 2.5
let otherRestaurantLocation = (8, 8)
let otherRestaurantRange = 2.5
// Pythagorean Theorem 📐🎓
func distance
from source: (x: Int, y: Int),
to target: (x: Int, y: Int)
) -> Double {
let distanceX = Double(source.x - target.x)
let distanceY = Double(source.y - target.y)
return (distanceX * distanceX +
distanceY * distanceY).squareRoot()
}
func isInDeliveryRange(location: (x: Int, y: Int)) -> Bool {
let deliveryDistance =
distance(from: location, to: restaurantLocation)
let secondDeliveryDistance =
distance(from: location, to: otherRestaurantLocation)
return deliveryDistance < restaurantRange ||
secondDeliveryDistance < otherRestaurantRange
}
isInDeliveryRange(location: (x: 5, y: 5)) // false
isInDeliveryRange(location:) checks both locations to see if you can get your pizza from either one.
Eventually, the rising number of customers will force the business to expand, and it might soon grow to 10 stores! Then what? Do you keep updating your function to check against all these sets of coordinates and ranges?
You might briefly consider creating an array of x/y coordinate tuples to keep track of your pizza restaurants, but that would be both difficult to read and maintain. Fortunately, Swift has additional tools to help you simplify the problem.
Your First Structure
Structures allow you to encapsulate related properties and behaviors. You can declare a new type, give it a name and then use it in your code.
In the pizza business example, you’ve used x/y coordinate tuples to represent locations.
As a first example of structures, promote locations from tuples to a structure type:
struct Location {
let x: Int
let y: Int
}
This block of code demonstrates the basic syntax for defining a structure. In this case, the code declares a type named Location that combines x and y coordinates.
The basic syntax begins with the struct keyword followed by the name of the type and a pair of curly braces. Everything between the curly braces is a member of the struct.
In Location, both members, x and y, are properties. Properties are constants or variables that are declared as part of a type. Every instance of this type will have these properties. In our example, every Location will have both an x and a y property.
You can instantiate a structure and store it in a constant or variable just like any other type you’ve worked with:
let storeLocation = Location(x: 3, y: 3)
To create the Location value, you use the name of the type along with a parameter list in parentheses. This parameter list provides a way to specify the values for the properties x and y. This is an example of an initializer.
Initializers enforce that all properties are set before you start using them. This guarantee is one of the key safety features of Swift. Accidentally using uninitialized variables is a significant source of bugs in other languages. Another handy Swift feature is that you don’t need to declare this initializer in the Location type. Swift automatically provides initializers for structures with all the properties in the parameter list. You’ll learn much more about initializers in Chapter 13, “Methods.”
You may remember that there’s also a range involved, and now that the pizza business is expanding, there may be different ranges associated with different restaurants. You can create another struct to represent the delivery area of a restaurant, like so:
struct DeliveryArea {
let center: Location
var radius: Double
}
var storeArea = DeliveryArea(center: storeLocation, radius: 2.5)
Now there’s a new structure named DeliveryArea that contains a constant center property along with a variable radius property. As you can see, you can have a structure value inside a structure value; here, you use the Location type as the type of the center property of the DeliveryArea struct.
Mini-Exercise
Write a structure that represents a pizza order. Include toppings, size and any other option you’d want for a pizza.
Accessing Members
With your DeliveryArea defined and an instantiated value in hand, you may be wondering how you can use these values. Just as you have been doing with Strings, Arrays, and Dictionaries, you use dot syntax to access members:
storeArea.radius // 2.5
You can even access members of members using dot syntax:
storeArea.center.x // 3
Similar to how you can read values with dot syntax, you can also assign them. If the delivery radius of one pizza location becomes larger, you could assign the new value to the existing property:
storeArea.radius = 3.5
Defining a property as a constant or variable determines if you can change it. In this case, you can modify radius because you declared it with var.
On the other hand, you declared center with let, so you can’t modify it. Your DeliveryArea struct allows a pizza restaurant’s delivery range to be changed, but not its location!
In addition to choosing whether your properties should be variable or constants, you must also declare the structure itself as a variable if you want to be able to modify it after it is initialized:
let fixedArea = DeliveryArea(center: storeLocation, radius: 4)
// Error: Cannot assign to property
fixedArea.radius = 3.5
Even though radius was declared with var, the enclosing type fixedArea is constant, so you can’t change it. The compiler correctly emits an error. Change fixedArea from a let constant to a var variable to make it mutable, so it compiles.
Now you’ve learned how to control the mutability of the properties in your structure.
Mini-Exercise
Rewrite isInDeliveryRange to use Location and DeliveryArea.
Introducing Methods
Using some of the capabilities of structures, you could now make a pizza delivery range calculator that looks something like this:
let areas = [
DeliveryArea(center: Location(x: 3, y: 3), radius: 2.5),
DeliveryArea(center: Location(x: 8, y: 8), radius: 2.5)
]
func isInDeliveryRange(_ location: Location) -> Bool {
for area in areas {
let distanceToStore =
distance(from: (area.center.x, area.center.y),
to: (location.x, location.y))
if distanceToStore < area.radius {
return true
}
}
return false
}
let customerLocation1 = Location(x: 5, y: 5)
let customerLocation2 = Location(x: 7, y: 7)
isInDeliveryRange(customerLocation1) // false
isInDeliveryRange(customerLocation2) // true
In this example, the function isInDeliveryRange() uses the areas array to determine if a customer’s location is within any of the delivery areas.
Being in range is something you want to know about for a particular restaurant. It’d be great if DeliveryArea could tell you if the restaurant could deliver to a location.
Much like a structure can have constants and variables, it can also define its own functions. In your playground, locate the implementation of DeliveryArea. Just before the closing curly brace and add the following code:
func contains(_ location: Location) -> Bool {
let distanceFromCenter =
distance(from: (center.x, center.y),
to: (location.x, location.y))
return distanceFromCenter < radius
}
This code defines a function contains as a member of DeliveryArea. Functions that are members of types are called methods. Notice how contains uses the center and radius properties of the current location. This implicit access to properties and other members inside the structure makes methods different from regular functions. You’ll learn more about methods in Chapter 13, “Methods”.
Just like other members of structures, you can use dot syntax to access a method:
let area = DeliveryArea(center: Location(x: 8, y: 8), radius: 2.5)
let customerLocation = Location(x: 7, y: 7)
area.contains(customerLocation) // true
Mini-Exercises
- Change
distance(from:to:)to useLocationas your parameters instead of x-y tuples. - Change
contains(_:)to call the newdistance(from:to:)withLocation. - Add a method
overlaps(with:)onDeliveryAreathat can tell you if the area overlaps with another area.
Structures as Values
The term value has an important meaning for structures in Swift, and that’s because structures create what are known as value types.
A value type is a type whose instances are copied on assignment.
var a = 5
var b = a
a // 5
b // 5
a = 10
a // 10
b // 5
This copy-on-assignment behavior means that when a is assigned to b, the value of a is copied into b. But later, when you change the value of a, the value of b stays the same. That’s why it’s important to read = as “assign”, not “is equal to”. Read the statement b = a as “Assign the value of a to b”.
Note: You use
==to calculate equality:2 + 2 == 4. Read this expression as a question: “Is 2 + 2 equal to 4?”.
How about the same principle, except with the DeliveryArea struct:
var area1 = DeliveryArea(center: Location(x: 3, y: 3), radius: 2.5)
var area2 = area1
area1.radius // 2.5
area2.radius // 2.5
area1.radius = 4
area1.radius // 4.0
area2.radius // 2.5
As with the previous example, area2.radius didn’t pick up the new value set in area1.radius. The disconnection demonstrates the value semantics of working with structures. When you assign area2 the value of area1, it gets an exact copy of this value. area1 and area2 are still completely independent!
Thanks to value semantics and copying, structures are safe, so you’ll never need to worry about values being shared and possibly being changed behind your back by another piece of code.
Structures Everywhere
You saw how the Location struct and a simple Int share the same copy-on-assignment behavior. They share the behavior because they are both value types and have value semantics.
You know structures represent values, so what exactly is an Int then? If you were to look at the definition of Int in the Swift library, you might be a bit surprised:
struct Int : FixedWidthInteger, SignedInteger {
// …
}
The Int type is also a structure. Many standard Swift types are structures, such as: Double, String, Bool, Array and Dictionary. As you’ll learn in future chapters, the value semantics of structures provide many other advantages over their reference type counterparts that make them ideal for representing core Swift types.
Conforming to a Protocol
You may have noticed some unfamiliar parts to the Int definition from the Swift standard library above. The types FixedWidthInteger and SignedInteger appear right after the declaration of Int:
struct Int : FixedWidthInteger, SignedInteger {
// …
}
These types are known as protocols. By putting them after a colon when Int is declared, you signal that Int conforms to these protocols.
Protocols contain a set of requirements that conforming types must satisfy. A simple example from the standard library is CustomStringConvertible:
public protocol CustomStringConvertible {
/// A textual representation of this instance.
var description: String { get }
}
This protocol contains one property requirement: description. The documentation refers to description as “A textual representation of this instance.”
If you were to modify DeliveryArea to conform to CustomStringConvertible, you would be required to add a description property with a “textual representation” of the instance. Try this now. Change DeliveryArea to:
struct DeliveryArea: CustomStringConvertible {
let center: Location
var radius: Double
var description: String {
"""
Area with center: (x: \(center.x), y: \(center.y)),
radius: \(radius)
"""
}
func contains(_ location: Location) -> Bool {
distance(from: center, to: location) < radius
}
func overlaps(with area: DeliveryArea) -> Bool {
distance(from: center, to: area.center) <=
(radius + area.radius)
}
}
The value of the description property contains the center and current radius. A value that updates in response to changes elsewhere is called a computed property.
You’ll learn all about computed properties — and more — in Chapter 12, “Properties”!
So what exactly does conforming to a protocol do? Because any type conforming to CustomStringConvertible must define description, so you can call description on any instance of any type that conforms to CustomStringConvertible. The Swift standard library takes advantage of this with the print() function. That function will use description in the console instead of a rather noisy default description:
print(area1) // Area with center: (x: 3, y: 3), radius: 4.0
print(area2) // Area with center: (x: 3, y: 3), radius: 2.5
Any named type can use protocols to extend its behavior. In this case, you conformed your structure to a protocol defined in the Swift standard library. In Chapter 17, “Protocols”, you’ll learn more about defining, using and conforming to protocols.
Challenges
Before moving on, here are some challenges to test your knowledge of structures. It’s best to try to solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.
Challenge 1: Fruit Tree Farm
Imagine you’re at a fruit tree farm and you grow different kinds of fruits: pears, apples and oranges. After the fruits are picked, a truck brings them in to be processed at the central facility. Since the fruits are all mixed together on the truck, the workers in the central facility have to sort them into the correct inventory container one by one.
Implement an algorithm that receives a truck full of different kinds of fruits and places each fruit into the correct inventory container.
Keep track of the total weight of fruit processed by the facility and print out how many of each fruit are in the inventory.
Challenge 2: A T-shirt Model
Create a T-shirt structure that has size, color and material options. Provide a method to calculate the cost of a shirt based on its attributes.
Challenge 3: Battleship
Write the engine for a Battleship-like game. If you aren’t familiar with Battleship, you can brush up on the details at this webpage: http://bit.ly/2nT3JBU
- Use an (x, y) coordinate system for your locations modeled using a structure.
- Ships should also be modeled with structures. Record an origin, direction and length.
- Each ship should be able to report if a “shot” has resulted in a “hit”.
Key Points
- Structures are named types you can define and use in your code.
- Structures are value types, which means their values are copied on assignment.
- You use dot syntax to access the members of named types, such as structures.
- Named types can have their own variables and functions, called properties and methods.
- Conforming to a protocol requires implementing the properties and methods required by that protocol.