Ione Souza Junior

Swift Essentials: Structs, Classes, and More (Part 3)

July 19, 2024 | 16 Minute Read | Translations: pt | #100DaysOfSwiftUI

In the first two parts of "100DaysOfSwiftUI", we explored the fundamental building blocks of Swift - variables, data types, control flow, loops, functions, and closures. Now, we're ready to dive into a crucial aspect of Swift programming: structs and classes. These powerful tools allow us to create custom data structures that organize and represent real-world objects in our code. Understanding structs and classes is essential for building complex and modular applications in SwiftUI, and they will empower you to create more powerful and robust iOS apps. Let's learn about it!

Structs: Value Types

Structs in Swift are value types that allow you to create custom data structures. Imagine them as blueprints for creating your own unique data containers, similar to how you might design a blueprint for a house.

Think of structs as “boxes” that hold related information. Each box has specific compartments (properties) to store different types of data, and you can define actions (methods) that can be performed on the data within the box. Here’s a breakdown of structs.

Basic Struct Definition

To define a struct, we use the struct keyword followed by the struct’s name:

struct User {
    // Properties and methods will go here
}

Let’s create a User struct to represent a user profile:

struct User {
    var firstName: String
    var lastName: String
}

This defines a User struct with two properties: firstName and lastName.

Stored Properties

Stored properties hold data within a struct. They act like variables inside the struct. The properties showed above are stored properties.

let userLaura = User(firstName: "Laura", lastName: "Campbell")

Computed Properties

Computed properties don’t store values directly. Instead, they calculate and return a value based on other properties.

struct User {
    var firstName: String
    var lastName: String

    var fullName: String {
        return "\(firstName) \(lastName)"
    }
}

let userLaura = User(firstName: "Laura", lastName: "Campbell")
print(userLaura.fullName) // Output: Laura Campbell

This User struct now has a fullName computed property that returns the concatenation of firstName and lastName.

Computed properties are very used on SwiftUI, mainly on views.

Property Observers (didSet, willSet)

Property observers allow you to perform actions whenever a property’s value changes. We have two:

  • didSet: This observer is called after a property’s value changes.
  • willSet: This observer is called before a property’s value changes.
struct User {
    var firstName: String
    var lastName: String {
        willSet {
            print("The new value is \(newValue)")
        }

        didSet {
            print("The old value is \(oldValue)")
        }
    }
}

var userLaura = User(firstName: "Laura", lastName: "Campbell")
userLaura.lastName = "Johnson"

// Output:
// The new value is Johnson
// The old value is Campbell

The newValue and oldValue are automatically powered by Swift when using these property observers. You can use this to develop something reactive. But be careful, because your code can be slow with a lot of actions in a simple property called many times.

Memberwise Initializers (Default Constructor)

Structs automatically get a memberwise initializer, which is a default initializer that allows you to create instances by providing values for all properties. As showed previously, we didn’t create any constructor for the struct, but we were capable to initialize the object and put values inside.

Custom Initializers

We can create our own custom initializers for structs to control instance creation based on specific parameters.

struct User {
    var firstName: String
    var lastName: String

    init(firstName: String, lastName: String) {
        self.firstName = firstName.uppercased()
        self.lastName = lastName.uppercased()
    }
}

Here, we create a custom initializer that takes the same parameters as the memberwise initializer. However, we can add additional logic or validation within the initializer before assigning values to the properties. If you create a custom initializer, you need to ensure that all property will be initialized with a value.

Mutating Methods

Methods that modify the properties of a struct needs the mutating keyword.

struct User {
    var firstName: String
    var lastName: String

    mutating func changeLastName(to newLastName: String) {
        lastName = newLastName
    }
}

var userLaura = User(firstName: "Laura", lastName: "Campbell")
userLaura.changeLastName(to: "Johnson")
print(userLaura.lastName) // Output: Johnson

Here, changeLastName modifies the lastName property, so it needs to be marked as mutating.

Structs are incredibly useful for creating custom data types, and understanding their features is crucial for building well-organized and efficient code in Swift. In the next section, we’ll dive into classes, another powerful tool for representing data and behavior in Swift.

Classes: Reference Types

Classes in Swift are like blueprints for creating objects that represent real-world entities. They’re similar to structs in that they can hold properties and methods, but they have some crucial differences. Classes are reference types, meaning that they’re passed around as references (pointers) instead of copies. This has significant implications for how they behave.

Let’s explore classes in more detail to understand some differences.

Basic Class Definition

To define a class, we use the class keyword followed by the class name. I’ll use the same example of User struct that I showed previously.

class User {

}

Let’s add some properties.

class User {
    var firstName: String
    var lastName: String

    init(firstName: String, lastName: String) {
        self.firstName = firstName.uppercased()
        self.lastName = lastName.uppercased()
    }
}

Notice now we need to specify the initializer method. Different from struct, Swift doesn’t create memberwise initialize for classes.

Inheritance

Inheritance allows us to create subclasses that inherit properties and methods from superclasses. This promotes code reusability and reduces redundancy.

class AdminUser: User {
    var canDeleteAccount: Bool

    init(firstName: String, lastName: String, canDeleteAccount: Bool) {
        self.canDeleteAccount = canDeleteAccount
        super.init(firstName: firstName, lastName: lastName)
    }
}

In this example, AdminUser inherits from User. It has its own property (canDeleteAccount) and overrides the initializer to take an additional canDeleteAccount parameter.

Note we’re using an initializer for inheritance class with super.init.

Methods

Different from structs, classes doesn’t need to be marked with the mutating keyword to change some property.

class User {
    var firstName: String
    var lastName: String

    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }

    func changeLastName(to newLastName: String) {
        lastName = newLastName
    }
}

var userLaura = User(firstName: "Laura", lastName: "Campbell")
userLaura.changeLastName(to: "Johnson")
print(userLaura.lastName) // Output: Johnson

Deinitializers

Deinitializers (deinit) are called when an instance of a class is about to be deallocated from memory. Some languages calling it as desctructors.

class User {
    var firstName: String
    var lastName: String

    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }

    deinit {
        print("\(firstName) is being deallocated.")
    }
}

To see it in action, try to use the playground and create this object in a loop. You’ll see when the context of class finishes, the deinit will be called.

for index in 1...3 {
    var userLaura = User(firstName: "Laura \(index)", lastName: "Campbell")
    print(userLaura.lastName)
}

// Output:
// Campbell
// Laura 1 is being deallocated.
// Campbell
// Laura 2 is being deallocated.
// Campbell
// Laura 3 is being deallocated.

Classes are powerful tools for building complex and object-oriented applications in Swift. They allow you to model real-world entities, promote code reusability through inheritance, and manage object lifecycle. But I believe you thinking now: when I need to use a struct or a class?

Struct or Class?

I’ll try to simplify: use a struct when you want to create simple data containers that are copied and remain immutable. Use a class when you want to create complex objects that are referenced and modified, and when inheritance is necessary.

Feature Struct (Value Type) Class (Reference Type)
Copying Copies are made when passed as arguments or assigned to new variables References are passed, so changes affect the original object
Mutability Properties are immutable by default. mutating keyword is required to modify properties within methods Properties are mutable by default. No need for mutating
Inheritance Structs cannot inherit from other structs or classes Classes can inherit from other classes (single inheritance)
Reference Counting No reference counting Reference counting is used to track object instances
Deinitializers Structs do not have deinitializers Classes can have deinitializers (deinit)
Typical Use Cases Representing simple data structures, value types, where immutability is preferred, data structures passed by value Representing complex objects, reference types, where inheritance or mutability is required, data structures passed by reference

In the table above I show you the main differences for each one. One thing that I haven’t showed yet is about “copying”. Let’s see it.

In the following code, I’ve created a user1 and copy it to a user2. I’ve changed the user1 property and print both. Note that the name are different for each one because we’re using a struct.

struct User {
    var firstName: String

    init(firstName: String) {
        self.firstName = firstName
    }
}

var user1 = User(firstName: "Laura")
var user2 = user1
user1.firstName = "Samantha"

print(user1.firstName) // Output: Samantha
print(user2.firstName) // Output: Laura

Now I just changed one keyword: I replaced the struct for a class and the result is different now.

class User {
    var firstName: String

    init(firstName: String) {
        self.firstName = firstName
    }
}

var user1 = User(firstName: "Laura")
var user2 = user1
user1.firstName = "Samantha"

print(user1.firstName) // Output: Samantha
print(user2.firstName) // Output: Samantha

This occurs because the differences between these two kinds of type. When you’re using a class and pass a reference of it, any changes affect the original object, but this is not the same for structs.

We already see a lot about structs and classes, right? Let’s see the next section to understand about the access control, which helps protect your code and ensure data integrity.

Access Control: Protecting Your Code

Access control in Swift is a mechanism that allows you to restrict access to entities within your code, like properties, methods, and even entire types. This helps ensure data integrity, prevent accidental modifications, and improve code organization.

Swift provides several access modifiers to control the visibility and accessibility of entities:

  • public: Accessible from anywhere, including other modules or frameworks.
  • private: Accessible only in the same scope where the entity is defined (like a class or an extensino).
  • fileprivate: Accessible only from within the same source file wher the entity is defined.

Also, for properties we can use the private (set) modifier to create properties only for public reading with private setter.

class User {
    private (set) var firstName: String

    init(firstName: String) {
        self.firstName = firstName
    }

    func deleteAccount() {
        deleteFiles()
        deleteData()
    }

    private func deleteFiles() {

    }

    fileprivate func deleteData() {

    }
}

Access control is particularly important in SwiftUI because it helps you control the data and behavior of your views. By carefully using access modifiers, you can create more robust and well-structured UI components.

This example is very simple, only to you understand what’s possible. Please, create a new playground project and explore it.

Static Properties and Methods

Static properties and methods are associated with the struct or class itself, rather than individual instances of the struct or class. This means that they can be accessed directly on the type without needing to create an instance.

struct User {
    static var totalOfUsers = 0

    var firstName: String

    init(firstName: String) {
        self.firstName = firstName
        User.totalOfUsers += 1
    }
}

print(User.totalOfUsers) // Output: 0
var user1 = User(firstName: "Laura")
print(User.totalOfUsers) // Output: 1
var user2 = User(firstName: "Samantha")
print(User.totalOfUsers) // Output: 2

Static properties hold data that is shared among all instances of a class or struct. So, be careful. Also, methods can be static too.

struct User {
    static var totalOfUsers = 0

    var firstName: String

    init(firstName: String) {
        self.firstName = firstName
        User.totalOfUsers += 1
    }

    static func describeTotalOfUsers() {
        print("There are \(totalOfUsers) users created.")
    }
}

User.describeTotalOfUsers() // Output: There are 0 users created.
var user1 = User(firstName: "Laura")
User.describeTotalOfUsers() // Output: There are 1 users created.

Wrap up

In this third installment of “100DaysOfSwiftUI,” we’ve delved into the world of structs and classes, the building blocks for organizing and representing data in Swift. We’ve explored their differences, learned how to define properties, methods, and initializers, and discovered about the access control.

By mastering structs and classes, we unlock new levels of power and flexibility in Swift programming. We can now model complex objects, create reusable code through inheritance, and control data access for better code organization and security.

As you continue your SwiftUI journey, remember that structs and classes are essential tools for building powerful and maintainable applications. They provide a structured approach to organizing your code, making it easier to manage, extend, and modify your projects. Structs are very important for SwiftUI, because they’re using massively to create views.

Now it’s your time. Take a time to practice and explore all described here using the playground, and practice using them in your own projects. The more you practice, the more confident you’ll become in harnessing their power.

Stay tuned for the next part of the “100DaysOfSwiftUI” series, where we’ll dive into more advanced topics, building upon the foundation we’ve laid together. Happy coding!