Why You Should Use Structs to Model Data and Logic in Swift

Photo by Marcello Gennari on Unsplash

Swift is a powerful and expressive programming language that offers many features to make your code more readable, maintainable, and performant. One of these features is structs, which are custom data types that can store multiple values of different types. Structs are also value types, which means that they are copied when they are assigned or passed around, rather than shared by reference. This makes them safer and more predictable than classes, which are reference types.

In this article, you will learn how to use structs to model data and logic in Swift, and why they are a great choice for many scenarios. You will also learn about some of the advantages and limitations of structs, and how to overcome them with extensions, protocols, and inheritance. By the end of this article, you will be able to create your own structs and use them effectively in your Swift projects.

What are structs and how to define them?

Structs are data types that can store multiple values of different types. For example, suppose you want to store the name and age of a person. You can create two variables: name and age, and assign values to them. However, suppose you want to store the same information for multiple people. In this case, creating variables for each person might be tedious and error-prone. To overcome this, you can create a struct that stores name and age as properties. Now, you can use this struct for every person.

To define a struct in Swift, you use the struct keyword followed by the name of the struct. Then, you place the properties and methods of the struct inside curly braces. Here is an example of a struct that represents a person:

struct Person {
    // define two properties
    var name: String
    var age: Int
}

Here, we have defined a struct named Person with two properties: name and age. The type of each property is specified after a colon. Note that we have used var to declare the properties as variables, which means that they can be changed after initialization. If you want to make them constants, you can use let instead.

How to create and use instances of structs?

A struct definition is just a blueprint. To use a struct, you need to create an instance of it. To create an instance of a struct in Swift, you use the name of the struct followed by an initializer. An initializer is a special method that sets up the initial state of an instance. Swift provides a default initializer for structs that takes the values of all the properties as parameters. Here is an example of creating an instance of the Person struct:

// create an instance of Person
var person1 = Person(name: "Alice", age: 25)

Here, we have created a variable called person1 and assigned it an instance of Person using the default initializer. We have passed the values for name and age as arguments to the initializer.

To access or modify the properties of an instance, you use dot notation. Dot notation is a syntax that allows you to access or modify a property or method of an instance by writing a dot (.) followed by the name of the property or method. Here is an example of accessing and modifying the properties of person1:

// access the name property
print(person1.name) // Alice
// modify the age property
person1.age = 26
// access the age property
print(person1.age) // 26

Here, we have used dot notation to print the name property, change the age property, and print the age property again.

You can also define methods inside structs to provide functionality related to the data they store. Methods are functions that are associated with instances of a type. To define a method in Swift, you write func followed by the name of the method and its parameters inside curly braces. Here is an example of defining a method called greet inside Person:

struct Person {
    // define two properties
    var name: String
    var age: Int
    // define a method
    func greet() {
        print("Hello, I'm \(name) and I'm \(age) years old.")
    }
}

Here, we have defined a method called greet that prints a greeting message using the name and age properties of the instance.

To call a method on an instance, you use dot notation as well. Here is an example of calling greet on person1:

// call greet on person1 
person1.greet() // Hello, I’m Alice and I’m 26 years old.

#Here, we have used dot notation to call greet on person1.

// create an instance of Person
var person1 = Person(name: "Alice", age: 25)

// create another instance by copying person1
var person2 = person1

// modify person2's name
person2.name = "Bob"

// print person1's name
print(person1.name) // Alice

// print person2's name
print(person2.name) // Bob

Why structs are value types and why it matters?

Structs are value types in Swift, which means that they are copied when they are assigned to a variable or constant, or passed as an argument to a function. This means that each instance of a struct has its own copy of the data, and modifying one instance does not affect another. Here is an example of copying a struct instance:

Here, we have created two instances of Person: person1 and person2. We have assigned person1 to person2, which creates a copy of person1. Then, we have modified person2’s name property. However, this does not affect person1’s name property, because they are two separate instances with their own copies of the data.

Value types are safer and more predictable than reference types, which are shared by reference rather than copied. Reference types can cause unexpected side effects when multiple variables or constants point to the same instance of a class. For example, if you pass a reference type instance to a function, and the function modifies that instance, the original instance is also modified. This can lead to bugs and confusion.

Structs also have better performance than classes, because copying structs is faster than allocating and deallocating memory for classes. Structs can also take advantage of compiler optimizations that eliminate unnecessary copies or use stack allocation instead of heap allocation.

What are some limitations of structs and how to overcome them?

Structs are great for modeling data and logic in Swift, but they also have some limitations that you should be aware of. One of these limitations is that structs cannot inherit from other structs. Inheritance is a feature that allows one class to inherit the characteristics of another class. Inheritance enables code reuse and polymorphism, which are important concepts in object-oriented programming.

However, structs can only conform to protocols, which are blueprints for methods, properties, and other requirements that suit a particular task or piece of functionality. Protocols can also provide default implementations for some or all of their requirements. By conforming to protocols, structs can adopt common behaviors and functionalities without inheriting from a base class.

For example, suppose you want to model different kinds of vehicles using structs. You can create a protocol called Vehicle that defines some common properties and methods for vehicles. Then, you can create different structs that conform to Vehicle and provide their own specific properties and methods. Here is an example:

// define a protocol for vehicles
protocol Vehicle {
    // define some properties
    var numberOfWheels: Int { get }
    var currentSpeed: Double { get set }

    // define some methods
    func start()
    func stop()
    func accelerate(by amount: Double)
    func decelerate(by amount: Double)
}

// provide default implementations for some methods
extension Vehicle {
    func start() {
        print("Starting...")
    }

    func stop() {
        print("Stopping...")
        currentSpeed = 0
    }

    func accelerate(by amount: Double) {
        print("Accelerating by \(amount)...")
        currentSpeed += amount
    }

    func decelerate(by amount: Double) {
        print("Decelerating by \(amount)...")
        currentSpeed -= amount
    }
}

// define a struct that conforms to Vehicle
struct Car: Vehicle {
    // provide specific properties
    var numberOfWheels = 4
    var currentSpeed = 0.0

    // provide specific methods
    func honk() {
        print("Beep beep!")
    }
}

// create an instance of Car
var car1 = Car()

// call methods from Vehicle protocol
car1.start()
car1.accelerate(by: 10)
car1.decelerate(by: 5)
car1.stop()

// call method from Car struct
car1.honk()

Here, we have defined a protocol called Vehicle that defines some common properties and methods for vehicles. We have also provided default implementations for some of the methods using an extension.

How to choose between structs and classes?

Structs and classes are both powerful and flexible constructs in Swift, but they have some important differences that affect your choice between them. Here are some guidelines to help you decide when to use structs or classes:

  • Use structs by default. Structs are simpler, safer, and more performant than classes. They also support many features that classes do, such as properties, methods, protocols, and extensions. Unless you need one of the features that only classes provide, such as inheritance, reference counting, or Objective-C interoperability, you should prefer structs over classes.

  • Use classes when you need inheritance. Inheritance is a feature that allows one class to inherit the characteristics of another class. Inheritance enables code reuse and polymorphism, which are important concepts in object-oriented programming. If you need to model a hierarchy of types that share common behaviors and attributes, you should use classes and inheritance. For example, if you want to create a custom view controller that inherits from UIViewController, you need to use a class.

  • Use classes when you need reference semantics. Reference semantics means that multiple variables or constants can refer to the same instance of a class. This allows you to share data and state across different parts of your app without copying or synchronizing. Reference semantics also enables identity comparison, which means that you can check if two references point to the same instance using the identity operator (===). If you need this kind of behavior, you should use classes. For example, if you want to model a file handle or a network connection that needs to be shared and controlled by different parts of your app, you should use a class.

Conclusion

In this article, you learned how to use structs to model data and logic in Swift, and why they are a great choice for many scenarios. You also learned about some of the advantages and limitations of structs, and how to overcome them with extensions, protocols, and inheritance. You also learned how to choose between structs and classes based on your needs and preferences.

Structs are one of the most powerful features of Swift, and they can help you write cleaner, safer, and faster code. By using structs wisely, you can make your Swift projects more expressive and enjoyable.

I hope you enjoyed this article and learned something new. If you have any questions or feedback, please let me know in the comments below. Thank you for reading!