Properties in Swift Explained

Written by LearnAppMaking on January 28 2021 in App Development, Swift

Properties in Swift Explained

You use properties in Swift to tack some data onto an object, like cookie.flavor = "Chocolate Chip". Properties in Swift look unassuming, but they actually pack quite a punch! In this tutorial, we’re going to discuss common aspects of working with properties.

Here’s what we’ll get into:

  • What’s a property and how do you use it?
  • Stored properties vs. computed properties
  • Customizing what happens when a property is set, with a setter
  • Customizing the return value of a property, with a getter
  • Observing property value changes with willSet and didSet

Ready? Let’s go.

  1. What’s a Property?
  2. Stored and Computed Properties
  3. Property Getters and Setters
  4. Property Observers: willSet and didSet
  5. Further Reading

What’s a Property?

A property is a value that’s connected to an object, like a class, struct or enum. You can think of them as “sub-variables”, i.e. a variable that’s part of another object.

Let’s take a look at an example:

struct Cookie {
    var flavor: String = ""
}

var choco = Cookie()
choco.flavor = "Chocolate Chip"

In the above code, we’ve defined a struct called Cookie. It has one property called flavor of type String, whose initial value is an empty string.

The syntax to declare a (stored) property is: var name: type = value. You do this within the class or struct, just as in the above example. You can also use let. The syntax is similar to that of declaring a variable or constant. In the next few sections we’ll discuss more approaches for working with properties.

In the second part of the above example code, we’ve initialized a Cookie object and assigned it to the choco variable. On the last line, we’re assigning a string value to the flavor property of the choco object.

You see that there are 2 parts to working with a property:

  1. Declaring the property inside a struct, with var flavor ···
  2. Assigning a value to the property, on an object, with choco.flavor = ···

Classes, structures and enumerations can all have properties. The syntax for properties with classes and structs is exactly the same. With an enum, you’d define the property with the case keyword. For each of these 3 constructs, you can get/set the property using dot-syntax, i.e. choco.flavor = ···.

Can you also use a property inside the class or struct? Yes! Check this out:

struct Cookie {
    var flavor: String = ""

    func eat() {
        print("** munch-munch ** Eating a \(flavor)...")
    }
}

var choco = Cookie()
choco.flavor = "Chocolate Chip Cookie"
choco.eat()
// Output: ** munch-munch ** Eating a Chocolate Chip cookie...

Within the eat() function, we’re reading the value from the flavor property. This happens inside the struct, and the function eat() is called from outside the struct. Implicitly, the eat() function uses the self keyword to get the value of the current Cookie object.

Access Control

Speaking of inside/outside – what about access control? You can determine if a property can be read from outside a struct or class, or only from inside it. You do this by placing one of these keywords before the property declaration.

  1. open and public: anyone can access, within the module, and in code that imports the module
  2. internal: anyone can access, but only within the module (default)
  3. fileprivate: anyone can access, but only within the current Swift file
  4. private: only the enclosing declaration can access, such as a class or struct

For example, if we wanted to restrict the getting and setting of the flavor property to only inside the Cookie struct, we’d change its declaration to this:

private var flavor: String = ""

Static Properties

You can also create static properties, by prepending the property declaration with the static keyword. A static property is available on the type of the struct (or class) itself, as opposed to an instance of that struct. As such, it’s often referred to as a “class property” and not an instance property.

Here’s an example:

struct Cookie {
    static var cookienessLevel = 0
    ···
}

Cookie.cookienessLevel = 9000
print(Cookie.cookienessLevel)
// Output: 9000

Awesome! Let’s move on to stored and computed properties.

Both the self keyword and access control are important concepts in Swift programming. You can learn more about them in Access Control Explained In Swift and Self and self in Swift.

Stored and Computed Properties

Properties come in 2 major flavors: stored properties and computed properties. A quick summary:

  • Stored property: A stored property merely stores a value. It has a type, and you can get/read and set/write the property. It’s as simple as that!
  • Computed property: A computed property returns a value based on some calculation. They’re read-only, unless you use a getter and setter (see below).

When developers talk about a property, they generally mean a stored property. Computed properties are in a league of their own, so they’re typically explicitly mentioned. Both stored and computed properties look the same from the outside, i.e. that cookie.flavor-like dot-syntax.

We’ve discussed how stored properties work in the previous section. Before we move on to computed properties, it’s worth noting here that a stored property can be lazy. When you prepend a property declaration with the lazy keyword, that property’s initial value will only be calculated when it’s first accessed.

A computed property calculates a value, rather than store it. Here’s an example:

struct Rectangle
{
    var width: Double
    var height: Double

    var area: Double {
        width * height
    }
}

let square = Rectangle(width: 12.0, height: 12.0)
print(square.area)
// Output: 144.0

In the above code, we’ve defined a struct called Rectangle. It has 2 stored properties, width and height of type Double. It also has a computed property called area of type Double.

In the second part of the example, we’ve created a Rectangle object and assigned it to the square constant. On the last line, we’re printing out the value of the area property.

The square.area code is used to get/read the value of the computed property area. When we do that, the code inside the squiggly brackets for the area property is executed. This code:

··· {
    width * height
}

It’s a calculation; a computation that’ll return the width × height surface area of the rectangle. The value of this calculation is implicitly returned by the property.

It also looks a bit like a function, doesn’t it? If we would have defined the same operation as a function, it would look like this:

func area() -> Double {
    return width * height
}

print(square.area())

A computed property is similar to a function, but it’s a property all the same. You can get its value just like any other property, and that makes for a concise and uniform API.

Let’s move on to getters and setters!

You can reference the name of a property, and pass that around in your code, with keypaths. Learn more here: Keypaths in Swift Explained

Property Getters and Setters

A computed property always defines a getter, and optionally defines a setter. In short, a getter is the code you use to get (“read”) a value from a property, and a setter is the code you use to set (“write”) a property to a new value. You can change the way that happens by customizing the getter and setter.

Let’s take a step back and look at how getters and setters work for stored properties. They don’t do anything else than storing and retrieving the value. Like this:

// Setting a value
cookie.flavor = "Chocolate Chip"

// Getting a value
print(cookie.flavor)

Getter

When you create a computed property, you’ll always provide a custom getter for that property. After all, you’re changing the way the property provides a value – with a computation.

Check out this computed property:

var area: Double {
    width * height
}

That’s exactly the same as this:

var area: Double {
    get {
        width * height
    }
}

With the get { ··· } code, we’ve indicated the code that needs to be executed to get/read a value from the area property. Because a computed property at least returns a value, this getter is implied when you don’t provide the get { ··· } block.

Setter

What about setters? They customize what happens when you change the value of the property. Here’s a comprehensive example:

struct Temperature {
    var celcius: Double = 0.0

    var fahrenheit: Double {
        get {
            (celcius * 9.0/5.0) + 32.0
        }
        set(newValue) {
            celcius = (newValue - 32.0) * 5.0/9.0
        }
    }
}

// 20 °C -> 68 °F
var temp = Temperature()
temp.celcius = 20
print(temp.fahrenheit)

// 86 °F -> 30 °C
temp.fahrenheit = 86
print(temp.celcius)

In the above code we’ve defined a structure Temperature with a stored property celcius and a computed property fahrenheit, both of type Double. The value of fahrenheit is based on that of celcius, so the celcius property is storing the actual temperature data.

You can see the getter for fahrenheit with get { ··· }, and you can also see the setter with set { ··· }. They’re almost the same, except that the setter has an additional value that’s available within it: newValue. It’s the new value that’s assigned to the computed property, when the setter is invoked.

In fact, you can name that value anything you want! You can even omit newValue altogether, and still use it inside the setter. Think of “newValue” as the “input” for the setter. When the code temp.fahrenheit = 86 is executed, 86 is the value that’s available as newValue within the setter.

You can also see that the setter of a computed property always involves another property, such as celcius. You cannot use a property’s value within its own getter or setter, because that’d cause an infinite loop.

Here, check this out:

struct Cookie {
    private var _flavor: String = ""

    var flavor: String {
        get {
            return _flavor
        }
        set(newFlavor) {
            _flavor = newFlavor
        }
    }
}

var cookie = Cookie()
cookie.flavor = "Chocolate Chip"
print(cookie.flavor)
// Output: Chocolate Chip

What’s going on here? If you look closely, you’ll see that we’ve defined a Cookie struct with two properties _flavor and flavor. The latter is a computed property that’ll store a value in the former. The computed property flavor will get/set from the private stored property _flavor.

We’ve essentially recreated a stored property using a computed property. This doesn’t make any sense; it’s not code you’d use during everyday programming. Still, it’s pretty neat – and it goes to show that getters and setters are just syntactic sugar around the storing/retrieving of data.

Property Observers: willSet and didSet

Before you go, let’s discuss one last powerful aspect of properties: property observers. Like the name implies, you can use property observers to respond to value changes of a property.

Here’s how that works:

  • Before a property changes, its willSet observer is invoked (with the new value as a parameter)
  • After a property changes, its didSet observer is invoked (with the old value as a parameter)

Let’s take a look at an example:

struct Receipt
{
    var amount: Int {
        willSet {
            print("About to set 'amount' from \(amount) to \(newValue)")
        }
        didSet {
            print("Did set 'amount' to \(amount), adding \(amount - oldValue)")
        }
    }
}

In the above code, we’ve defined a struct called Receipt. It has one stored property amount of type Int. To this property we’ve added a willSet and didSet property observer.

  • In willSet, you can use the about-to-be-set value called newValue
  • In didSet, you can use the previous value called oldValue
  • In both property observers you can use the current value, e.g. amount

Here’s what that struct looks like in action:

var receipt = Receipt(amount: 0)
receipt.amount = 100
receipt.amount = 133
receipt.amount = 42

// About to set 'amount' from 0 to 100
// Did set 'amount' to 100, adding 100
// About to set 'amount' from 100 to 133
// Did set 'amount' to 133, adding 33
// About to set 'amount' from 133 to 42
// Did set 'amount' to 42, adding -91

See how the property observers inform you when the value of a property has changed? You also have access to the complete story. You can follow every mutation, from what a value was to what it will be, and so on.

A few notes worth making:

  • You can add willSet and didSet to stored properties that you define and inherit, and to computed properties you inherit. If you’ve defined your own computed property, it’s common to add code that observes it to the setter instead of an observer. So you may not need didSet or willSet for your specific use case!
  • If you’re setting properties in a custom initializer, those properties’ willSet and didSet aren’t called. The property observers of a superclass you inherit are executed, however, once the superclass initializer has been called. (You’ll know it when you see it. It’s good to know that observers behave tricky around initializers.)

Further Reading

Properties look so simple, but there’s a lot of complexity behind some simple code like cookie.flavor = "Chocolate Chip". You’ve got plenty of opportunities to customize how your properties behave.

Here’s what we discussed:

  • What a stored property is and how you use it
  • Stored properties vs. computed properties
  • Getters and setters with get and set(newValue)
  • Property observers with willSet and didSet

Want to learn more? Check out these resources:

LearnAppMaking

LearnAppMaking

At LearnAppMaking.com, app developers learn how to build and launch awesome iOS apps.