Chapters

Hide chapters

Swift Apprentice: Fundamentals

First Edition · iOS 16 · Swift 5.7 · Xcode 14.2

Section III: Building Your Own Types

Section 3: 9 chapters
Show chapters Hide chapters

11. Structures
Written by Ehab Amer

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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 typestructures. 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”.

X Y You Pizza 0 1 2 3 4 5 6 7 8 9 10 10 9 8 7 6 5 4 3 2 1 0 😃 🍕

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.

X Y 0 1 2 3 4 5 6 7 8 9 10 10 9 8 7 6 5 4 3 2 1 0 😃 🍕

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?

X Y 0 1 2 3 4 5 6 7 8 9 10 10 9 8 7 6 5 4 3 2 1 0 😃 🍕 🍕

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.

struct Location {
  let x: Int
  let y: Int
}
let storeLocation = Location(x: 3, y: 3)
struct DeliveryArea {
  let center: Location
  var radius: Double
}

var storeArea = DeliveryArea(center: storeLocation, radius: 2.5)

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
storeArea.center.x // 3
storeArea.radius = 3.5
let fixedArea = DeliveryArea(center: storeLocation, radius: 4)

// Error: Cannot assign to property
fixedArea.radius = 3.5

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
func contains(_ location: Location) -> Bool {
  let distanceFromCenter =
    distance(from: (center.x, center.y),
             to: (location.x, location.y))

  return distanceFromCenter < radius
}
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

  1. Change distance(from:to:) to use Location as your parameters instead of x-y tuples.
  2. Change contains(_:) to call the new distance(from:to:) with Location.
  3. Add a method overlaps(with:) on DeliveryArea that 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.

var a = 5
var b = a

a // 5
b // 5

a = 10

a // 10
b // 5
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

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.

struct Int : FixedWidthInteger, SignedInteger {
 // …
}

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 {
  // …
}
public protocol CustomStringConvertible {
  /// A textual representation of this instance.
  var description: String { get }
}
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)
  }
}
print(area1) // Area with center: (x: 3, y: 3), radius: 4.0
print(area2) // Area with center: (x: 3, y: 3), radius: 2.5

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.

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

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.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now