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 named Point.
  • var x: Double and var y: Double are called stored properties. They hold values specific to each Point instance. We use var because 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 named Person.
  • var name: String and var age: Int are 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 takes name and age as parameters.
  • self.name = name assigns the name parameter to the name property of the Person instance 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.

flowchart TD subgraph Value_Type_Struct["Value Type "] A[Original Struct Instance] B[Copied Struct Instance] A -->|\1| B B -->|\1| B_Modified[Modified Copy] A -->|\1| A_Unchanged[Original Unchanged] end subgraph Reference_Type_Class["Reference Type "] C[Single Class Instance in Memory] D[Variable 1] E[Variable 2] D -->|\1| C E -->|\1| C D -->|\1| C E -->|\1| C end Value_Type_Struct --- Analogy_Value["Photocopy of a Document"] Reference_Type_Class --- Analogy_Reference["Link to a Shared Document"]

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 UserManager or a NetworkClient).
  • When the type represents an identity (e.g., a specific ViewController or a DatabaseConnection).

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 with var. This is because let makes 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 its var properties.

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 UserProfile as a struct.
  • id is a let constant because a user’s ID should generally not change after creation.
  • username and email are var because a user might update them.
  • registrationDate is also a let constant, 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: "[email protected]", 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 Foundation is necessary to use the Date type.
  • let user1 = ... creates a constant instance of UserProfile.
  • 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 = "[email protected]"

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 "[email protected]"

Explanation:

  • var user2 = user1 creates a distinct copy of user1’s data.
  • Modifying user2.username and user2.email only affects the user2 instance, leaving user1 untouched. 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:

  • UserSession is a class.
  • It holds a UserProfile (our struct!) as a property. This shows how structs and classes can work together.
  • isAuthenticated and lastActivity are var because 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 the lastActivity property. No mutating keyword 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 a UserSession instance.
  • var session2 = session1 makes session2 point to the exact same UserSession instance that session1 points to. No new instance is created.
  • When we modify session2.isAuthenticated or call session2.updateLastActivity(), these changes are applied to the single UserSession object in memory.
  • Therefore, when we print session1’s details again, we see the changes made via session2. 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:

  1. A Product struct. It should have id (String, let), name (String, let), price (Double, var), and stock (Int, var).
  2. A ShoppingCart class. It should contain an array of Product structs (or copies of them). It should have methods to addProduct(product: Product) and getTotalPrice() -> Double.

Challenge:

  1. Define the Product struct.
  2. Define the ShoppingCart class with an initializer and the two methods.
  3. Create an instance of Product.
  4. Create two ShoppingCart instances.
  5. Add the same Product instance to both shopping carts.
  6. Modify the price of the original Product instance.
  7. Calculate and print the total price for both shopping carts.
  8. Observe: Does changing the original Product affect 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

  1. Forgetting mutating for Struct Methods: If you define a method within a struct that intends to change one of its properties, you must mark that method with the mutating keyword. Forgetting this will result in a compiler error: “Cannot assign to property: ‘self’ is immutable.”
  2. 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.
  3. Confusing let with Classes: Remember that let for 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 also let within 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.
  • mutating Keyword: Required for struct methods that modify the struct’s own properties, signifying that the method will change the value of the instance.
  • let vs. var: For structs, let makes the entire instance immutable. For classes, let makes the reference immutable, but the properties of the referenced object can still be modified if they are var.

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

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.