Swift Tutorial: Introducing Structures
Learn about structures, a fundamental compound type in Swift that let you define your own types – complete with data and actions! By Erik Kerber.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Swift Tutorial: Introducing Structures
15 mins
Introducing Self
If a method parameter and property have the same name, you can use the self
keyword to give the compiler explicit context if you want to operate on the property. When you use self
in code, you're explicitly accessing the current value of the named type.
In other words, using dot synax on self
is just like using dot syntax on a variable storing that value.
self.latitude = atof(String(crdSplit.first!))
You're not required to use self
when writing code within a named type; Swift infers it automatically, which is why you’ve been able to write code without it so far.
Why use it then? It's especially useful in initializers: When two variables of the same name exist in the same scope, self
can prevent what's known as shadowing.
init(latitude: Double, longitude: Double) {
latitude = latitude
longitude = longitude
}
There's an initializer argument latitude
as well as a property latitude
. Which one are you referring to in this code snippet?
It's actually the argument, which isn't what you want. Instead, you can simply add self.
before the variable you're assigning, as follows:
init(latitude: Double, longitude: Double) {
self.latitude = latitude
self.longitude = longitude
}
This makes it clear that you're assigning the parameter values to the properties.
Initializer Rules
Initializers in structs have a few rules that guard against unset values. By the end of the initializer, the struct must have initial values set in all of its stored properties.
If you were to forget to assign the value of longitude
, for instance, you would see an error at compile time:
Its purpose is simple: Stored properties need to be initialized with a value.
Other languages have types similar to structs and may assign default values, such as a value of 0 for an integer. Swift, focusing on safety, ensures that every value has been explicitly set.
There's one exception to the rule that stored properties must have values: optionals!
struct ClimateControl {
var temperature: Double
var humidity: Double?
init(temp: Double) {
temperature = temp
}
}
In this simplified example, humidity is an optional—perhaps an “optional” setting on a thermostat! Here, the initializer doesn’t specify a value for humidity
. Because humidity
is an optional, the compiler will happily oblige.
For convenience, you can specify another initializer that would include both values:
struct ClimateControl {
var temperature: Double
var humidity: Double?
init(temp: Double) {
temperature = temp
}
init(temp: Double, hum: Double) {
temperature = temp
humidity = hum
}
}
Now you can create ClimateControl
values with or without a humidity value:
let ecoMode = ClimateControl(temp: 75.0)
let dryAndComfortable = ClimateControl(temp: 71.0, hum: 30.0)
Optional variables do get a default value of nil
, which means the first initializer with only a temperature parameter is valid.
nil
or an actual value. Once the struct is defined, it can no longer be changed!
Introducing Methods
Using some of the capabilities of structs, you could now make a pizza delivery range calculator that looks something like this:
let pizzaJoints = [
DeliveryRange(location: Location(coordinateString: "44.9871,-93.2758")),
DeliveryRange(location: Location(coordinateString: "44.9513,-93.0942")),
]
func isInRange(location: customer) -> Bool {
for pizzaRange in pizzaJoints {
let difference = sqrt(pow((latitude - lat), 2) + pow((longitude - long), 2))
if (difference < joint.range) {
return true
}
}
return false
}
let customer = Location(“44.9850,-93.2750")
print(isInRange(location: customer)) // Pizza time!
In this example, there's an array pizzaJoints
and a function that uses that array to determine if a customer’s location is within range of any of them.
The idea of being "in range" is very tightly coupled to the characteristics of a single pizza restaurant. In fact, all of the calculations in isInRange
occur on one location at a time. Wouldn’t it be great if DeliveryRange
itself could tell you if the restaurant can deliver to a certain customer?
Much like a struct can have constants and variables, it can also define its own functions:
struct DeliveryRange {
var range: Double
let center: Location
func isInRange(customer: Location) -> Bool {
let difference = sqrt(pow((customer.latitude - center.latitude), 2) +
pow((customer.longitude - center.longitude), 2))
return difference < range
}
}
This code defines the method isInRange(_:)
, which is now a member of DeliveryRange
. In Swift, methods are simply functions that are associated with a type. Just like other members of structs, you can use dot syntax to access a method through a value of its associated type:
let range = DeliveryRange(range: 150, center: Location(coordinateString: "44.9871,-93.2758"))
let customer = Location(coordinateString: "44.9850,-93.2750")
range.isInRange(customer) // true!
Structures as Values
The term value has an important meaning when it comes to structs in Swift, and that's because structs create what are known as value types.
A value type is an object or a piece of data that is copied on assignment, which means the assignment gets an exact copy of the data rather than a reference to the very same data.
// Assign the literal ‘5’ into a, an Int.
var a: Int = 5
// Assign the value in a to b.
var b: Int = a
print(a) // 5
print(b) // 5
// Assign the value ’10’ to a
a = 10
// a now has ’10’, b still has ‘5’
print(a) // 10
print(b) // 5
Simple, right! Obvious, even?
Notice that even though a
and b
have the same value to start, changing the value of a
later doesn't affect b
. They're separate variables with separate values.
How about the same principle, except with the Location
struct:
// Build a DeliveryRange value
var range1: DeliveryRange = DeliveryRange(range: 200, center: Location(“44.9871,-93.2758”))
// Assign the value in range1 to range2
var range2: DeliveryRange = range1
print(range1.range) // 200
print(range2.range) // 200
// Modify the range of range1 to ‘100’
range1.range = 100
// range1 now has ’100’, b still has ‘200’
print(range1.range) // 100
print(range2.range) // 200
As with the Int
example, range2
didn't pick up the new value set in range1
. The important thing is that this demonstrates the value semantics of working with structs. When you assign range2
the value from range1
, it gets an exact copy of the value. That means you can modify range1
without also modifying the range in range2
.