Skip to main content
  1. SwiftUI in 100 Days Notes/

Day 10 - Swift Struct - 1 : Struct, Computed Property and Property Observer

How to Create a Struct? #

The struct in Swift allows us to create a comprehensive, custom data type with its own variables and its own functions.

A simple struct looks like the following;

struct Album {
    let title: String
    let artist: String
    let year: Int

    func printSummary() {
        print("\(title) (\(year)) by \(artist)")
    }
}

The code above creates a new type called Album. This type contains two Strings, title and artist, an Int named year and a function named printSummary().

When naming a Struct, the first letter is capitalized. Note that Album in our example also starts with a capital letter.

Now we can create variables and constants of type Album, assign values to them or copy them. Just like we created String or Int before.

let red = Album(title: "Red", artist: "Taylor Swift", year: 2012)
let wings = Album(title: "Wings", artist: "BTS", year: 2016)

print(red.title)
print(wings.artist)

red.printSummary()
wings.printSummary()

//OUTPUT:
//----------------------------------------
//Red
//BTS
//Red (2012) by Taylor Swift
//Wings (2016) by BTS

We can create new data of type Album like calling a function. As you can see, although both red and wings are of type Album, they are completely separate from each other.

We can see this when we call printSummary() on each struct. When we call printSummary() on red and wing based on the same struct, they return different values.

Where things differ is when we want to have values that can change. For example, we can create an Employee struct that can take a vacation when needed.

struct Employee {
    let name: String
    var vacationRemaining: Int

    func takeVacation(days: Int) {
        if vacationRemaining > days {
            vacationRemaining -= days
            print("I'm going on vacation!")
            print("Days remaining: \(vacationRemaining)")
        } else {
            print("Oops! There aren't enough days remaining.")
        }
    }
}

When we want to write the code above, Swift will not want to build the code.

struct_mutable_function

Swift creates structs and their data as constants. This makes Swift run faster. But when a function in a struct wants to change some data, it is not allowed to do so, because all the data is immutable.

So, if we have a function that modifies the data of struct, it should be marked with mutating. If we have a function that does not modify the data, but only reads it, it can do its job without any problem and does not need to be marked with mutating.

mutating func takeVacation(days: Int) {

After making the above change in our Struct code, it will work just fine. Let’s create a constant from Employee struct:

let archer = Employee(name: "Sterling Archer", vacationRemaining: 14)
archer.takeVacation(days: 5)
print(archer.vacationRemaining)

When we want to generate the above code, we will get an error again. Because when we define a struct with a mutating function as a constant, the data cannot be changed.

mutable function and let

The valid code should be as follows.

var archer = Employee(name: "Sterling Archer", vacationRemaining: 14)
archer.takeVacation(days: 5)
print(archer.vacationRemaining)

Important points about Struct;

  • Constants and variables of a Struct are called property.
  • Functions belonging to a struct are called method.
  • When a constant or variable is created from a struct, it is called an instance.
  • When creating an instance from a struct, we use an initializer like the following.
    • Album(title: "Wings", artist: "BTS", year: 2016)

When creating an instance from a struct, we actually use the init function. But since Swift does this for us, we don’t create an instance by typing init. This is called syntactic sugar. Both of the following code examples do the same thing, and create instance from the Employee struct.

var archer1 = Employee(name: "Sterling Archer", vacationRemaining: 14)
var archer2 = Employee.init(name: "Sterling Archer", vacationRemaining: 14)

The init() function is created automatically according to the property we set when creating the struct.

For example, we have 2 property in our struct;

let name: String
var vacationRemaining = 14

Swift defaults to 14 for vacationRemaining if we don’t specify it during init.

let kane = Employee(name: "Lana Kane")
let poovey = Employee(name: "Pam Poovey", vacationRemaining: 35)

But there is an important point to note here: If we define any struct property as a constant (let) and assign a value to it, this property will not appear in the init function. (Because a constant can only be assigned a value once.) In order to use default value assignment, we must use a variable (var).

Struct Computed Property #

Structs can have two kinds of property.

Stored Property : A variable (var) or constant (let) that holds a piece of data within an instance of the Struct

Computed Property : The value of the property is dynamically computed each time it is accessed. This makes computed property a mixture of stored property and function. It is accessed like stored property but works like a function.

Let’s use the simplified version of our earlier Employee struct as our example.

struct Employee {
    let name: String
    var vacationRemaining: Int
}

var archer = Employee(name: "Sterling Archer", vacationRemaining: 14)
archer.vacationRemaining -= 5
print(archer.vacationRemaining)
archer.vacationRemaining -= 3
print(archer.vacationRemaining)

//OUTPUT:
//----------------------------------------
//9
//6

The above struct works, but we lose some valuable information. When we created the struct, we kept the number of vacation rights of the employee in the variable vacationRemainig. But as the employee takes leave, we lose the information about the employee’s vacation rights.

We can overcome this problem by using computed property.

struct Employee {
    let name: String
    var vacationAllocated = 14
    var vacationTaken = 0

    var vacationRemaining: Int {
        vacationAllocated - vacationTaken
    }
}

Now instead of assigning vacationRemaining directly, we calculate vacationRemaining by subtracting the leave used from the vacation right.

When we want to learn vacationRemaining we can read it like a standard stored property.

var archer = Employee(name: "Sterling Archer", vacationAllocated: 14)
archer.vacationTaken += 4
print(archer.vacationRemaining)
archer.vacationTaken += 4
print(archer.vacationRemaining)

//OUTPUT:
//----------------------------------------
//10
//6

This is a really powerful feature. It looks like a normal property but when we want to read it, calculations are done.

IMPORTANT NOTE : Constants (let) cannot be computed property (Why? 😁 because constants can be assigned values only once).

But we can’t write data to the vacationRemaining property, because we haven’t told Swift how to do that. To do that, we need to provide a getter and a setter in this property. Getter means the code that reads the value and Setter means the code that writes the value.

Let’s add getter and setter to our Employee struct.

var vacationRemaining: Int {
    get {
        vacationAllocated - vacationTaken
    }

    set {
        vacationAllocated = vacationTaken + newValue
    }
}

get and set can be written as in the example above. The important thing here is newValue. This is automatically provided to us by Swift and stores the value that the user wants to assign to property.

After providing getter and setter we can change vacationRemaining as we want.

var archer = Employee(name: "Sterling Archer", vacationAllocated: 14)
archer.vacationTaken += 4
archer.vacationRemaining = 5
print(archer.vacationAllocated)

//OUTPUT:
//----------------------------------------
//9

Property Observer #

A piece of code that runs when a property changes is called a property observer. Property observer can be in two ways: didSet when the property changes, willSet observer before the property changes.

To understand why we would need Property obsever, let’s consider a code like the one below;

struct Game {
    var score = 0
}

var game = Game()
game.score += 10
print("Score is now \(game.score)")
game.score -= 3
print("Score is now \(game.score)")
game.score += 1

//OUTPUT:
//----------------------------------------
//Score is now 10
//Score is now 7

In the code above, the score property is modified and after each change the current score is printed with print. But there is a problem: nothing is printed after the last score change.

Let’s write the same code with property observer.

struct Game {
    var score = 0 {
        didSet {
            print("Score is now \(score)")
        }
    }
}

var game = Game()
game.score += 10
game.score -= 3
game.score += 1

//OUTPUT:
//----------------------------------------
//Score is now 10
//Score is now 7
//Score is now 8

There is also the Swift provided constant oldValue which can be used in didSet. Of course there is also the constant newValue which is provided automatically in willSet.

struct App {
    var contacts = [String]() {
        willSet {
            print("Current value is: \(contacts)")
            print("New value will be: \(newValue)")
        }

        didSet {
            print("There are now \(contacts.count) contacts.")
            print("Old value was \(oldValue)")
        }
    }
}

var app = App()
app.contacts.append("Adrian E")
app.contacts.append("Allen W")
app.contacts.append("Ish S")

//OUTPUT:
//----------------------------------------
//Current value is: []
//New value will be: ["Adrian E"]
//There are now 1 contacts.
//Old value was []
//Current value is: ["Adrian E"]
//New value will be: ["Adrian E", "Allen W"]
//There are now 2 contacts.
//Old value was ["Adrian E"]
//Current value is: ["Adrian E", "Allen W"]
//New value will be: ["Adrian E", "Allen W", "Ish S"]
//There are now 3 contacts.
//Old value was ["Adrian E", "Allen W"]

You need to be careful when using didSet and willSet. Because putting too much work on the property observer can cause performance problems.

Note : Property observer is not used with constants (let) (because the value of constants is set only once)

How to Create Struct Custom Initializer? #

Initiliazer are special methods designed to prepare a new struct instance to be used. We have already seen how they are automatically generated based on the property we put into the struct. But we can also create our own custom initializer as long as we follow the rule. Rule of thumb: All property must have a value at the end of the initializer.

Let’s look at the automatically generated initializer;

struct Player {
    let name: String
    let number: Int
}

let player = Player(name: "Megan R", number: 15)

Swift creates an initializer for a new Player instance with two existing properties by default. In Swift, this is called a memberwise initializer.

We can also write the init function ourselves.

struct Player {
    let name: String
    let number: Int

    init(name: String, number: Int) {
        self.name = name
        self.number = number
    }
}

The code above does the same thing as the code we just wrote. But here the initializer belongs to us and we can add functionality if we want.

Some things to be aware of;

  • There is no func keyword. Syntactically it looks like a function, but Swift privileges the initializer here.
  • Although init creates a new Player instance, initializers do not have a fixed return type.
  • We use the keyword self to distinguish between name used in struct and name used in init (self.name is the property).

Of course, a custom initializer does not have to work like the automatically generated memberwise initializer. For example, the name variable can be given by the user, while the number variable can be randomly assigned.

struct Player {
    let name: String
    let number: Int

    init(name: String) {
        self.name = name
        number = Int.random(in: 1...99)
    }
}

let player = Player(name: "Megan R")
print(player.number)

All properties must have a value at the end of the initializer. If the variable number was not assigned a value, the above code would throw an error.

We can add more than one initializer to the struct, and we can take advantage of features like external parameter name and default value. It is important to note that when we create a custom initializer, we lose access to the memberwise initializer that is automatically generated by Swift, unless otherwise specified.

Using Memberwise Initializer and Custom Initializer Together #

We mentioned that if we use a custom initializer, the memberwise initializer is deprecated. But in Swift we can create an exception to this.

To create the exception, we will use extension.

struct Employee {
    var name: String
    var yearsActive = 0
}

extension Employee {
    init() {
        self.name = "Anonymous"
        print("Creating an anonymous employee…")
    }
}

// We can create roslin by specifying the name variable.
let roslin = Employee(name: "Laura Roslin")

// We can create the anon variable without specifying any variable.
// in this case the name variable will be Anonymous.
let anon = Employee()

You can also read this article in Turkish.
Bu yazıyı Türkçe olarak da okuyabilirsiniz.

This article contains the notes I took for myself from the articles found at SwiftUI Day 10. Please use the link to follow the original lesson.