Introduction
Welcome back, future Swift maestros! In the previous chapters, we laid the groundwork with variables, constants, basic data types, and functions. Now, it’s time to level up our ability to organize and model data in a meaningful way. Imagine trying to describe a person, a car, or a recipe using just individual variables – it would quickly become a tangled mess!
This chapter introduces two of Swift’s most fundamental building blocks for creating custom data types: structs and classes. These powerful constructs allow us to bundle related properties (data) and methods (functions that operate on that data) into a single, cohesive unit. Understanding structs and classes is absolutely crucial for writing clean, efficient, and idiomatic Swift code, especially as you embark on building production-grade iOS applications.
By the end of this chapter, you’ll not only know how to define and use structs and classes but, more importantly, you’ll grasp the critical distinction between value types and reference types. This concept is a cornerstone of Swift programming and will profoundly influence your architectural decisions. Get ready to build your own custom data models from the ground up!
Core Concepts: Blueprints for Your Data
Think of structs and classes as blueprints. Just like a blueprint for a house defines its rooms, windows, and doors, a struct or class blueprint defines the characteristics (properties) and behaviors (methods) of a specific type of data.
Let’s start with the basics of defining them.
Defining a Struct
A struct (short for structure) is a versatile, lightweight type that you can use to create custom data types. They are ideal for modeling simple data values.
Here’s the basic syntax:
struct SomeStructure {
// Properties go here
// Methods go here
}
Let’s imagine we want to model a Point in a 2D coordinate system. A point has an x coordinate and a y coordinate.
struct Point {
var x: Double
var y: Double
}
In this Point struct:
struct Point { ... }declares a new structure namedPoint.var x: Doubleandvar y: Doubleare called stored properties. They hold values specific to eachPointinstance. We usevarbecause the coordinates of a point might change.
Defining a Class
A class is another fundamental building block, similar to a struct, but with some key differences we’ll explore shortly. Classes are often used for more complex entities that might involve inheritance or shared mutable state.
Here’s the basic syntax:
class SomeClass {
// Properties go here
// Methods go here
}
Now, let’s model a Person. A person has a name and an age.
class Person {
var name: String
var age: Int
}
In this Person class:
class Person { ... }declares a new class namedPerson.var name: Stringandvar age: Intare also stored properties.
Creating Instances (Objects)
Once you have a blueprint (struct or class definition), you can create actual “things” based on that blueprint. These “things” are called instances or objects.
To create an instance, you use an initializer. Swift provides a default memberwise initializer for structs if you don’t define your own, allowing you to set all properties when creating an instance. For classes, you typically need to define your own initializer or provide default values for all properties.
Let’s create an instance of our Point struct:
let origin = Point(x: 0.0, y: 0.0)
let myLocation = Point(x: 10.5, y: 20.0)
Here, origin and myLocation are instances of the Point struct. We’re using the default memberwise initializer Point(x:y:) to set their initial values.
Now for our Person class. Since we haven’t given name and age default values, we need to provide an initializer.
class Person {
var name: String
var age: Int
// This is an initializer
init(name: String, age: Int) {
self.name = name // 'self' refers to the current instance
self.age = age
}
}
let alice = Person(name: "Alice", age: 30)
let bob = Person(name: "Bob", age: 25)
In this class:
init(name: String, age: Int)is our custom initializer. It takesnameandageas parameters.self.name = nameassigns thenameparameter to thenameproperty of thePersoninstance being initialized.
You can access properties of an instance using dot syntax:
print("Origin X coordinate: \(origin.x)") // Output: Origin X coordinate: 0.0
print("Alice's name: \(alice.name)") // Output: Alice's name: Alice
alice.age = 31 // We can change Alice's age because 'age' is a 'var' and 'alice' is a 'let' (but we'll learn why this works for classes shortly!)
print("Alice's new age: \(alice.age)") // Output: Alice's new age: 31
Wait, why could we change alice.age when alice itself was declared with let? This brings us to the most crucial difference between structs and classes: Value Types vs. Reference Types.
The Big Difference: Value Types vs. Reference Types
This is where structs and classes diverge significantly, and understanding this distinction is paramount for writing correct and predictable Swift code.
Value Types (Structs)
When you create a struct, you’re creating a value type. This means that when you assign a struct instance to a new variable or pass it to a function, a copy of that instance is made. Each variable holds its own unique copy of the data.
Think of it like getting a photocopy of a document. You have the original, and someone else has a copy. If they highlight something on their copy, your original remains unchanged.
struct Point {
var x: Double
var y: Double
}
var p1 = Point(x: 1.0, y: 2.0)
var p2 = p1 // p2 now has a *copy* of p1's data
print("p1: (\(p1.x), \(p1.y))") // p1: (1.0, 2.0)
print("p2: (\(p2.x), \(p2.y))") // p2: (1.0, 2.0)
p2.x = 5.0 // Modify p2
print("After modifying p2.x:")
print("p1: (\(p1.x), \(p1.y))") // p1: (1.0, 2.0) - Unchanged!
print("p2: (\(p2.x), \(p2.y))") // p2: (5.0, 2.0)
Notice how changing p2.x had no effect on p1.x. This is the essence of a value type.
Reference Types (Classes)
When you create a class instance, you’re creating a reference type. This means that when you assign a class instance to a new variable or pass it to a function, you’re not copying the instance itself, but rather creating another reference (or pointer) to the same single instance in memory. Both variables now point to the same underlying data.
Think of it like sharing a link to a Google Doc. Everyone who has the link is looking at and potentially editing the same document. If one person makes a change, everyone sees it.
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
var person1 = Person(name: "Charlie", age: 40)
var person2 = person1 // person2 now refers to the *same* instance as person1
print("Person1 name: \(person1.name), age: \(person1.age)") // Charlie, 40
print("Person2 name: \(person2.name), age: \(person2.age)") // Charlie, 40
person2.age = 41 // Modify person2 (which is the same instance as person1)
print("After modifying person2.age:")
print("Person1 name: \(person1.name), age: \(person1.age)") // Charlie, 41 - Changed!
print("Person2 name: \(person2.name), age: \(person2.age)") // Charlie, 41
Here, changing person2.age also changed person1.age because both person1 and person2 refer to the exact same Person object in your computer’s memory. This is why let alice = Person(...) earlier allowed alice.age = 31. let on a class instance means the reference itself cannot be changed to point to a different instance, but the properties of the instance it points to can still be modified if they are declared as var.
Visualizing Value vs. Reference Types
Let’s use a simple diagram to solidify this concept.
When to Use Which? Apple’s Recommendation
This is a frequently asked question for Swift developers. Apple’s general guidance is: “Prefer structs over classes.”
Why? Because structs, being value types, offer several advantages:
- Predictability: You know that when you pass a struct around, you’re working with a unique copy. This prevents unexpected side effects from other parts of your code modifying the same instance.
- Thread Safety: Since copies are made, structs are inherently safer in multi-threaded environments (concurrency), reducing the risk of race conditions.
- Performance: For small data models, structs can sometimes offer better performance because they are stored directly where they are used, potentially reducing memory overhead and improving cache locality.
So, when should you use a struct?
- When modeling simple data values (e.g.,
Point,Size,Color,DateRange). - When the data doesn’t need to be inherited by other types.
- When you want copies to be independent.
- When the type represents a unique value, not an identity.
And when should you use a class?
- When you need inheritance: Classes can inherit characteristics from other classes. Structs cannot.
- When you need Objective-C interoperability: Many Apple frameworks are built on Objective-C, which uses classes extensively. Your Swift classes can seamlessly interact with Objective-C code.
- When you need shared mutable state: If you explicitly want multiple parts of your code to refer to and potentially modify the same instance of data (e.g., a shared
UserManageror aNetworkClient). - When the type represents an identity (e.g., a specific
ViewControlleror aDatabaseConnection).
Mutability and mutating Methods for Structs
When working with structs, the var and let keywords interact a bit differently than with classes.
- If you declare a struct instance with
let, you cannot change any of its properties, even if those properties themselves are declared withvar. This is becauseletmakes the entire value immutable.struct Rectangle { var width: Double var height: Double } let myRectangle = Rectangle(width: 10.0, height: 5.0) // myRectangle.width = 12.0 // ❌ ERROR: Cannot assign to property: 'myRectangle' is a 'let' constant - If you declare a struct instance with
var, you can change itsvarproperties.
What if a struct needs to have a method that modifies one of its own properties? For example, a Point struct might have a method to move itself.
struct Point {
var x: Double
var y: Double
// A method that modifies a property of the struct instance
mutating func move(byX deltaX: Double, byY deltaY: Double) {
x += deltaX
y += deltaY
}
}
var myPoint = Point(x: 1.0, y: 2.0)
print("Before move: (\(myPoint.x), \(myPoint.y))") // Before move: (1.0, 2.0)
myPoint.move(byX: 3.0, byY: -1.0)
print("After move: (\(myPoint.x), \(myPoint.y))") // After move: (4.0, 1.0)
// let fixedPoint = Point(x: 0, y: 0)
// fixedPoint.move(byX: 1, byY: 1) // ❌ ERROR: Cannot use mutating member on immutable value: 'fixedPoint' is a 'let' constant
The mutating keyword is essential here! It tells Swift that this method intends to modify the properties of the struct instance it’s called on. Without mutating, the compiler would prevent x += deltaX because structs are immutable by default when passed around or when their instance is a let constant.
Classes don’t need the mutating keyword because their instances are reference types. If you have a var property in a class, you can always modify it through any reference to that instance, regardless of whether the method is declared mutating or not (the concept doesn’t apply to classes).
Step-by-Step Implementation: Building a User Profile
Let’s put these concepts into practice by building a simple UserProfile and a UserSession to see structs and classes in action.
Step 1: Define a UserProfile Struct
We’ll start with a UserProfile struct. This will hold immutable data about a user, like their ID, name, and email. Since user profiles are often treated as distinct data values, a struct is a great fit.
Open your Swift environment (like an Xcode playground or a Swift file) and add the following:
// Step 1: Define a UserProfile Struct
struct UserProfile {
let id: String
var username: String
var email: String
let registrationDate: Date // We'll use Swift's built-in Date type
}
Explanation:
- We declare
UserProfileas astruct. idis aletconstant because a user’s ID should generally not change after creation.usernameandemailarevarbecause a user might update them.registrationDateis also aletconstant, as it’s typically set once.
Step 2: Create a UserProfile Instance
Now, let’s create an instance of our UserProfile. Swift automatically provides a memberwise initializer for structs.
// Step 2: Create a UserProfile Instance
import Foundation // Needed for the Date type
let user1 = UserProfile(id: "u123", username: "swift_learner", email: "learner@example.com", registrationDate: Date())
print("User 1 Profile:")
print("ID: \(user1.id)")
print("Username: \(user1.username)")
print("Email: \(user1.email)")
print("Registration Date: \(user1.registrationDate)")
Explanation:
import Foundationis necessary to use theDatetype.let user1 = ...creates a constant instance ofUserProfile.- We use the memberwise initializer
UserProfile(id:username:email:registrationDate:)to set all initial values.
Step 3: Demonstrate Value Type Behavior with UserProfile
Let’s create a copy of user1 and modify it. Observe how the original user1 remains unaffected.
// Step 3: Demonstrate Value Type Behavior
var user2 = user1 // user2 now holds a *copy* of user1
user2.username = "swift_master"
user2.email = "master@example.com"
print("\nUser 2 Profile (Modified):")
print("Username: \(user2.username)")
print("Email: \(user2.email)")
print("\nUser 1 Profile (Original should be unchanged):")
print("Username: \(user1.username)") // Should still be "swift_learner"
print("Email: \(user1.email)") // Should still be "learner@example.com"
Explanation:
var user2 = user1creates a distinct copy ofuser1’s data.- Modifying
user2.usernameanduser2.emailonly affects theuser2instance, leavinguser1untouched. This is the core characteristic of a value type.
Step 4: Define a UserSession Class
Now, let’s consider a UserSession. This might represent the active login state of a user, which needs to be shared and potentially modified across different parts of an application. This is a good candidate for a class.
// Step 4: Define a UserSession Class
class UserSession {
let userProfile: UserProfile // A session has a user profile
var isAuthenticated: Bool
var lastActivity: Date
init(userProfile: UserProfile, isAuthenticated: Bool, lastActivity: Date) {
self.userProfile = userProfile
self.isAuthenticated = isAuthenticated
self.lastActivity = lastActivity
}
func updateLastActivity() {
self.lastActivity = Date()
print("User session activity updated for \(userProfile.username).")
}
}
Explanation:
UserSessionis aclass.- It holds a
UserProfile(our struct!) as a property. This shows how structs and classes can work together. isAuthenticatedandlastActivityarevarbecause they will change during the session.- We define a custom initializer
init(...)because classes don’t get a default memberwise initializer like structs do. updateLastActivity()is a method that modifies thelastActivityproperty. Nomutatingkeyword is needed here because it’s a class method.
Step 5: Create a UserSession Instance and Demonstrate Reference Type Behavior
Let’s create a session and then have another variable refer to the same session.
// Step 5: Create a UserSession Instance and Demonstrate Reference Type Behavior
let session1 = UserSession(userProfile: user1, isAuthenticated: true, lastActivity: Date())
print("\nSession 1 Details:")
print("User: \(session1.userProfile.username), Authenticated: \(session1.isAuthenticated), Last Activity: \(session1.lastActivity)")
var session2 = session1 // session2 now refers to the *same* instance as session1
session2.isAuthenticated = false // Modify through session2
session2.updateLastActivity() // Call a method that modifies the instance
print("\nSession 2 Details (Modified):")
print("User: \(session2.userProfile.username), Authenticated: \(session2.isAuthenticated), Last Activity: \(session2.lastActivity)")
print("\nSession 1 Details (Original should also be changed):")
print("User: \(session1.userProfile.username), Authenticated: \(session1.isAuthenticated), Last Activity: \(session1.lastActivity)")
Explanation:
let session1 = ...creates a constant reference to aUserSessioninstance.var session2 = session1makessession2point to the exact sameUserSessioninstance thatsession1points to. No new instance is created.- When we modify
session2.isAuthenticatedor callsession2.updateLastActivity(), these changes are applied to the singleUserSessionobject in memory. - Therefore, when we print
session1’s details again, we see the changes made viasession2. This is the hallmark of a reference type.
Mini-Challenge: Model a Product and a Shopping Cart
Now it’s your turn! Based on what you’ve learned, create two custom types:
- A
Productstruct. It should haveid(String, let),name(String, let),price(Double, var), andstock(Int, var). - A
ShoppingCartclass. It should contain an array ofProductstructs (or copies of them). It should have methods toaddProduct(product: Product)andgetTotalPrice() -> Double.
Challenge:
- Define the
Productstruct. - Define the
ShoppingCartclass with an initializer and the two methods. - Create an instance of
Product. - Create two
ShoppingCartinstances. - Add the same
Productinstance to both shopping carts. - Modify the
priceof the originalProductinstance. - Calculate and print the total price for both shopping carts.
- Observe: Does changing the original
Productaffect the price in the shopping carts? Why or why not?
Hint: Remember that structs are value types. What happens when you add a struct to an array?
// Your code for the Mini-Challenge goes here!
import Foundation // For Date if you used it, otherwise not strictly needed for this challenge
// 1. Define Product struct
struct Product {
let id: String
let name: String
var price: Double
var stock: Int
}
// 2. Define ShoppingCart class
class ShoppingCart {
var items: [Product] = [] // An array to hold products
init() {
// No custom properties to initialize, so an empty initializer is fine
}
func addProduct(product: Product) {
items.append(product)
print("Added '\(product.name)' to cart.")
}
func getTotalPrice() -> Double {
var total: Double = 0.0
for item in items {
total += item.price
}
return total
}
}
// 3. Create an instance of Product
var laptop = Product(id: "LPT001", name: "SuperLaptop Pro", price: 1500.00, stock: 10)
print("\nOriginal Laptop Price: \(laptop.price)")
// 4. Create two ShoppingCart instances
let cartA = ShoppingCart()
let cartB = ShoppingCart()
// 5. Add the same Product instance to both shopping carts
cartA.addProduct(product: laptop) // This adds a *copy* of 'laptop' to cartA
cartB.addProduct(product: laptop) // This adds another *copy* of 'laptop' to cartB
// 6. Modify the price of the *original* Product instance
laptop.price = 1200.00 // Sale!
print("Modified Original Laptop Price (on sale): \(laptop.price)")
// 7. Calculate and print the total price for both shopping carts
print("\nCart A Total Price: \(cartA.getTotalPrice())")
print("Cart B Total Price: \(cartB.getTotalPrice())")
// 8. Observe: Does changing the original Product affect the price in the shopping carts? Why or why not?
// The price in the shopping carts should NOT change.
// This is because Product is a struct (value type). When 'laptop' was added to 'items' array
// in each ShoppingCart, a *copy* of the 'laptop' struct was made and stored in the array.
// Modifying the original 'laptop' instance afterwards has no effect on those copies.
// If 'Product' were a class, then changing 'laptop.price' would affect the instances
// referenced by the shopping carts, as they would all point to the same object in memory.
Common Pitfalls & Troubleshooting
- Forgetting
mutatingfor Struct Methods: If you define a method within a struct that intends to change one of its properties, you must mark that method with themutatingkeyword. Forgetting this will result in a compiler error: “Cannot assign to property: ‘self’ is immutable.” - Unexpected Side Effects with Reference Types: This is probably the most common source of bugs when working with classes. If you have multiple variables or parts of your code referencing the same class instance, a change made through one reference will be visible to all others. This can lead to difficult-to-track bugs where data mysteriously changes. Always be mindful when working with shared class instances.
- Confusing
letwith Classes: Remember thatletfor a class instance means the reference itself cannot be changed to point to a different object. It does not mean the properties of the object it points to are immutable (unless those properties are alsoletwithin the class).class MyClass { var value = 0 } let myInstance = MyClass() myInstance.value = 10 // This is perfectly fine! The *instance* is mutable. // myInstance = MyClass() // ❌ ERROR: Cannot assign to value: 'myInstance' is a 'let' constant
Summary
Phew! You’ve just tackled one of the most fundamental and important distinctions in Swift programming. Let’s recap the key takeaways:
- Structs and Classes are blueprints for creating custom data types, bundling properties and methods.
- Structs are Value Types: When copied or passed, a new, independent copy of the data is made. This makes them predictable and often safer.
- Classes are Reference Types: When copied or passed, a new reference (pointer) to the same single instance in memory is created. Changes made through one reference are seen by all other references.
- Apple’s Recommendation: “Prefer structs” for most data modeling, especially when dealing with simple, independent values.
- Use Classes for: Inheritance, Objective-C interoperability, managing shared mutable state, or when object identity is important.
mutatingKeyword: Required for struct methods that modify the struct’s own properties, signifying that the method will change the value of the instance.letvs.var: For structs,letmakes the entire instance immutable. For classes,letmakes the reference immutable, but the properties of the referenced object can still be modified if they arevar.
You now have a solid understanding of how to model data in Swift and, more importantly, the crucial implications of choosing between structs and classes. This knowledge will be invaluable as you design the data architecture of your iOS applications.
What’s Next? In the next chapter, we’ll dive deeper into controlling the flow of your program with decision-making tools like if/else, switch, and loops. Get ready to make your programs smarter!
References
- The Swift Programming Language Guide - Structures and Classes
- Apple Developer Documentation - Choosing Between Structures and Classes
- Swift Evolution - SE-0068: Expanding
selfto class and protocol method types (Relevant for advanced understanding ofself) - Mermaid.js Official Documentation
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.