Let’s talk about structures, or structs as they’re known in code. Structures are a fundamental building block in Swift. They are versatile and widely used, from simple data containers to more complex types with their own behavior.

What are Structures?

A structure is a way to group related values together into a named type. Think of it like a blueprint for creating structures that hold specific kinds of data. For example, you could define a struct to represent a 2D point with x and y coordinates, or a struct to hold information about a book, like its title, author, and pageCount.

In Swift, structures are value types. This is an important concept that we’ll explore more, but it essentially means that when you pass a struct around in your code (e.g. to a function or assign it to a new variable), a copy of its data is made.


Defining a Structure

You define a structure using the struct keyword, followed by its name and a pair of braces. Inside the braces, you define its properties.

Example: A Point Structure

struct Point {
    var x: Double
    var y: Double
}

This defines a new type called Point that has two stored properties: x and y, both of type Double.

Creating Instances (Objects)

Once you’ve defined a structure, you can create instances of it. Swift provides an automatic memberwise initialiser for structures if you don’t define your own custom initialisers. This means if you don’t define an init, you can initialise the structure with its existing properties.

// Create an instance of Point
var origin = Point(x: 0.0, y: 0.0)

// Access its properties
print("The origin is at (\(origin.x), \(origin.y))") // Prints "The origin is at (0.0, 0.0)"

// Modify its properties (since 'origin' is a variable)
origin.x = 10.0
origin.y = 20.0
print("The point is now at (\(origin.x), \(origin.y))") // Prints "The point is now at (10.0, 20.0)"

If you create an instance as a constant (using let), its properties cannot be changed after initialisation, even if they were declared as variables (var) in the struct definition.

let fixedPoint = Point(x: 5.0, y: 5.0)
// fixedPoint.x = 10.0 // This would cause a compile-time error

Structures are Value Types

This is a key characteristic of structures in Swift. When a value type is assigned to a new constant or variable, or when it’s passed to a function, a copy of its value is made. This means you’re working with an independent instance, and changes to one copy won’t affect others.

Example: Copying Behavior

struct Rectangle {
    var width: Int
    var height: Int
}

var rectA = Rectangle(width: 100, height: 50)
var rectB = rectA // rectB is a COPY of rectA

// Modify rectB
rectB.width = 120

print("rectA width: \\(rectA.width)") // Prints "rectA width: 100"
print("rectB width: \\(rectB.width)") // Prints "rectB width: 120"

As you can see, changing rectB.width did not affect rectA.width because rectB is an independent copy.

This behaviour is different from reference types (like classes), where assigning an instance to a new variable or passing it to a function creates a shared reference to the same underlying instance.

When to Use Value Types (Structs)

Value types are particularly useful when:

  • You want to ensure that instances have independent state.
  • The data encapsulated is relatively small and simple.
  • You need thread-safety without complex locking mechanisms (as copies don’t share memory in the same way references do).
  • You are modeling data that doesn’t have a distinct identity or lifecycle beyond its values (e.g., a coordinate, a color, a date range).

Many of Swift’s basic types like Int, Double, String, Array, and Dictionary are implemented as structures and exhibit value type behavior.


Adding Behavior with Methods

Structures can have their own functions, called methods, to encapsulate behavior related to the data they hold.

Example: Rectangle with an area method

struct Rectangle {
    var width: Double
    var height: Double

    // Instance method to calculate the area
    func area() -> Double {
        width * height
    }

    // Mutating method to scale the rectangle
    mutating func scale(by factor: Double) {
        width *= factor
        height *= factor
    }
}

var myRectangle = Rectangle(width: 10.0, height: 5.0)
print("Area: \\(myRectangle.area())") // Prints "Area: 50.0"

myRectangle.scale(by: 2.0)
print("Scaled width: \\(myRectangle.width), Scaled height: \\(myRectangle.height)")
// Prints "Scaled width: 20.0, Scaled height: 10.0"

Mutating Methods

If an instance method needs to modify the properties of the structure, you must mark it with the mutating keyword. This is because, by default, instance methods on value types cannot change the instance’s properties. The mutating keyword signals that the method is allowed to change the instance it’s called on.


Initialisers

Initialisers are special methods responsible for setting up a new instance of a type. We saw the memberwise initializer that Swift provides automatically. You can also define your own custom initialisers.

Example: Temperature with Custom Initialisers

struct Temperature {
    var celsius: Double

    // Initialize from Celsius
    init(celsius: Double) {
        self.celsius = celsius
    }

    // Initialize from Fahrenheit
    init(fahrenheit: Double) {
        self.celsius = (fahrenheit - 32.0) / 1.8
    }

    // Computed property to get Fahrenheit
    var fahrenheit: Double {
        (celsius * 1.8) + 32.0
    }
}

let boilingPoint = Temperature(celsius: 100.0)
print("\\(boilingPoint.celsius)°C is \\(boilingPoint.fahrenheit)°F")
// Prints "100.0°C is 212.0°F"

let freezingPoint = Temperature(fahrenheit: 32.0)
print("\\(freezingPoint.celsius)°C is \\(freezingPoint.fahrenheit)°F")
// Prints "0.0°C is 32.0°F"

Computed Properties

In addition to stored properties (which store a value directly), structures can have computed properties. These don’t store a value themselves but calculate it based on other properties. The fahrenheit property in the Temperature example above is a computed property.

Example: Circle with Computed diameter and circumference

struct Circle {
    var radius: Double

    // Computed property for diameter
    var diameter: Double {
        get {
            return radius * 2
        }
        set(newDiameter) {
            radius = newDiameter / 2
        }
    }

    // Read-only computed property for circumference
    var circumference: Double {
        2 * Double.pi * radius
    }
}

var myCircle = Circle(radius: 5.0)
print("Radius: \\(myCircle.radius)") // Prints "Radius: 5.0"
print("Diameter: \\(myCircle.diameter)") // Prints "Diameter: 10.0"
print("Circumference: \\(myCircle.circumference)") // Prints "Circumference: 31.4159..."

// Change the diameter, and the radius will update
myCircle.diameter = 20.0
print("New radius: \\(myCircle.radius)") // Prints "New radius: 10.0"

Computed properties can have a getter (get) and an optional setter (set). If only a getter is provided, it’s a read-only computed property.


When to Choose Structures vs. Classes

Swift offers both structures (struct) and classes (class) for defining custom types. The primary difference is that structs are value types and classes are reference types. Here’s a quick guideline:

  • Use structures by default.
  • Use classes when you need Objective-C interoperability.
  • Use classes when you need to control the identity of an instance (i.e., you want to ensure that multiple references point to the exact same object in memory).
  • Use classes when you need inheritance (structs don’t support inheritance).
  • Use classes when you need a deinitialiser (deinit) to clean up resources.

Apple’s general recommendation is to prefer structures because they are simpler to reason about due to their value semantics (no side effects from shared mutable state).


Wrapping Up

Structures are a cornerstone of Swift development, providing a flexible and efficient way to create custom data types. Their value type semantics offer safety and predictability, making them suitable for a wide range of applications, from simple data aggregation to complex models with custom behavior.

By understanding how to define structures, add properties and methods, and leverage their value type nature, you can write cleaner, more robust Swift code.

Happy coding!