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

12. Properties
Written by Eli Ganim

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

Chapter 11, “Structures”, showed that you can use structures to group related properties and behaviors into a custom type.

In the example below, the Car structure has two properties; both are constants that store String values:

struct Car {
  let make: String
  let color: String
}

The values inside a structure are called properties. The two properties of Car are stored properties, which means they store actual string values for each instance of Car.

Some properties calculate values rather than store them. In other words, there’s no actual memory allocated for them; instead, they get calculated on-the-fly each time you access them. Naturally, these are called computed properties.

In this chapter, you’ll learn about both kinds of properties. You’ll also learn some other neat tricks for working with properties, such as how to monitor changes in a property’s value and delay the initialization of a stored property.

Stored Properties

As you may have guessed from the example in the introduction, you’re already familiar with the features of stored properties.

To review, imagine you’re building an address book. You’ll need a Contact type:

struct Contact {
  var fullName: String
  var emailAddress: String
}

You can use this structure repeatedly, letting you build an array of contacts, each with a different value. The properties you want to store are an individual’s full name and email address.

Contact fullName emailAddress

These are the properties of the Contact structure. You provide a data type for each but opt not to assign a default value because you plan to assign the value upon initialization. After all, the values will differ for each instance of Contact.

Remember that Swift automatically creates an initializer for you based on the properties you defined in your structure:

var person = Contact(fullName: "Grace Murray",
                 emailAddress: "grace@navy.mil")

You can access the individual properties using dot notation:

person.fullName // Grace Murray
person.emailAddress // grace@navy.mil

You can assign values to properties as long as they’re defined as variables and the parent instance is stored in a variable. That means both the property and the structure containing the property must be declared with var instead of let.

When Grace married, she changed her last name:

person.fullName = "Grace Hopper"
person.fullName // Grace Hopper

Since the property is a variable, she could update her name.

If you’d like to prevent a value from changing, you can define a property as a constant using let, like so:

struct Contact {
  var fullName: String
  let emailAddress: String
}

// Error: cannot assign to a constant
person.emailAddress = "grace@gmail.com"

Once you’ve initialized an instance of this structure, you can’t change emailAddress.

Default Values

If you can make a reasonable assumption about the value of a property when the type is initialized, you can give that property a default value.

struct Contact {
  var fullName: String
  let emailAddress: String
  var relationship = "Friend"
}
var person = Contact(fullName: "Grace Murray",
                     emailAddress: "grace@navy.mil")
person.relationship // Friend

var boss = Contact(fullName: "Ray Wenderlich",
                   emailAddress: "ray@kodeco.com",
                   relationship: "Boss")

Computed Properties

Most of the time, properties are stored data, but some can just be computed, which means they perform a calculation before returning a value.

struct TV {
  var height: Double
  var width: Double

  // 1
  var diagonal: Int {
    // 2
    let result = (height * height +
      width * width).squareRoot().rounded()
    // 3
    return Int(result)
  }
}
var tv = TV(height: 53.93, width: 95.87)
tv.diagonal // 110
tv.width = tv.height
tv.diagonal // 76

Mini-Exercise

Do you have a television or a computer monitor? Measure the height and width, plug it into a TV struct, and see if the diagonal measurement matches what you think it is.

Getter and Setter

The computed property you wrote in the previous section is called a read-only computed property. It has a block of code to compute the property’s value, called the getter.

var diagonal: Int {
  // 1
  get {
    // 2
    let result = (height * height +
      width * width).squareRoot().rounded()
    return Int(result)
  }
  set {
    // 3
    let ratioWidth = 16.0
    let ratioHeight = 9.0
    // 4
    let ratioDiagonal = (ratioWidth * ratioWidth +
      ratioHeight * ratioHeight).squareRoot()
    height = Double(newValue) * ratioHeight / ratioDiagonal
    width = height * ratioWidth / ratioHeight
  }
}
tv.diagonal = 70
tv.height // 34.32...
tv.width // 61.01...

Type Properties

In the previous section, you learned how to declare stored and computed properties for instances of a particular type. The properties on your instance of TV are separate from the properties on my instance of TV.

struct Level {
  let id: Int
  var boss: String
  var unlocked: Bool
}

let level1 = Level(id: 1, boss: "Chameleon", unlocked: true)
let level2 = Level(id: 2, boss: "Squid", unlocked: false)
let level3 = Level(id: 3, boss: "Chupacabra", unlocked: false)
let level4 = Level(id: 4, boss: "Yeti", unlocked: false)
struct Level {
  static var highestLevel = 1
  let id: Int
  var boss: String
  var unlocked: Bool
}
// Error: you can’t access a type property on an instance
let highestLevel = level3.highestLevel
Level.highestLevel // 1

Property Observers

For your Level implementation, it would be useful to automatically set the highestLevel when the player unlocks a new one. For that, you’ll need a way to listen to property changes. Thankfully, there are a couple of property observers that get called before and after property changes.

struct Level {
  static var highestLevel = 1
  let id: Int
  var boss: String
  var unlocked: Bool {
    didSet {
      if unlocked && id > Self.highestLevel {
        Self.highestLevel = id
      }
    }
  }
}

Limiting a Variable

You can also use property observers to limit the value of a variable. Say you had a light bulb that could only support a maximum current flowing through its filament.

struct LightBulb {
  static let maxCurrent = 40
  var current = 0 {
    didSet {
      if current > LightBulb.maxCurrent {
        print("""
              Current is too high,
              falling back to previous setting.
              """)
        current = oldValue
      }
    }
  }
}
var light = LightBulb()
light.current = 50
light.current // 0
light.current = 40
light.current // 40

Mini-Exercise

In the light bulb example, the bulb goes back to a successful setting if the current gets too high. In real life, that wouldn’t work, and the bulb would burn out! Your task is to rewrite the structure so the bulb turns off before the current burns it out.

Lazy Properties

If you have a property that might take some time to calculate, you don’t want to slow things down until you need the property. Say hello to the lazy stored property. It is useful for such things as downloading a user’s profile picture or making a serious calculation.

struct Circle {
  lazy var pi = {
    ((4.0 * atan(1.0 / 5.0)) - atan(1.0 / 239.0)) * 4.0
  }()
  var radius = 0.0
  var circumference: Double {
    mutating get {
      pi * radius * 2
    }
  }
  init(radius: Double) {
    self.radius = radius
  }
}
Ziguar G o k j e q x e g a s y e

var circle = Circle(radius: 5) // got a circle, pi has not been run
circle.circumference // 31.42
// also, pi now has a value

Mini-Exercises

Of course, you should trust the value of pi from the standard library. It’s a type property, and you can access it as Double.pi. Given the Circle example above:

Challenges

Before moving on, here are some challenges to test your knowledge of properties. It is 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: Ice Cream

Rewrite the IceCream structure below to use default values and lazy initialization:

struct IceCream {
  let name: String
  let ingredients: [String]
}

Challenge 2: Car and Fuel Tank

At the beginning of the chapter, you saw a Car structure. Dive into the inner workings of the car and rewrite the FuelTank structure below with property observer functionality:

struct FuelTank {
  var level: Double // decimal percentage between 0 and 1
}

Key Points

  • Properties are variables and constants that are part of a named type.
  • Stored properties allocate memory to store a value.
  • Computed properties are calculated each time your code requests them and aren’t stored as a value in memory.
  • The static modifier marks a type property that’s universal to all instances of a particular type.
  • The lazy modifier prevents a value of a stored property from being calculated until your code uses it for the first time. You’ll want to use lazy initialization when a property’s initial value is computationally intensive or when you won’t know the initial value of a property until after you’ve initialized the object.
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