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.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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:

image001

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.

Note: If you declare the optional as a constant, you must still provide an initial value, whether it's 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.

Erik Kerber

Contributors

Erik Kerber

Author

Over 300 content creators. Join our team.