Let’s talk about structures, or struct
s 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!