Swift Structures and Classes - Building Custom Types
Welcome to Swift Structures and Classes! These are the building blocks for creating custom types in Swift. Both structures and classes allow you to encapsulate data and functionality into reusable, organized units. In this guide, we’ll explore how to use both, understand their differences, and learn when to choose one over the other.
What Are Structures and Classes?
Structures (structs) and classes are blueprints for creating custom types. They let you group related data and functionality together.
Common Features (Both Have):
- ✅ Properties to store values
- ✅ Methods to define functionality
- ✅ Initializers to set up initial state
- ✅ Subscripts for collection-style access
- ✅ Extensions to add functionality
- ✅ Protocol conformance
Class-Only Features:
- 🎯 Inheritance (classes can inherit from other classes)
- 🎯 Type casting to check and interpret types
- 🎯 Deinitializers to clean up resources
- 🎯 Reference counting (multiple references to same instance)
Key Difference:
- Structs are value types (copied when assigned)
- Classes are reference types (shared when assigned)
Defining Structures
Basic Structure
struct Person { var name: String var age: Int}
// Create an instancevar person = Person(name: "Alice", age: 25)print("\(person.name) is \(person.age) years old")// Output: Alice is 25 years old
// Modify propertiesperson.age = 26print("\(person.name) is now \(person.age)")// Output: Alice is now 26Structure with Methods
struct Rectangle { var width: Double var height: Double
func area() -> Double { return width * height }
func perimeter() -> Double { return 2 * (width + height) }}
let rect = Rectangle(width: 10, height: 5)print("Area: \(rect.area())") // Area: 50.0print("Perimeter: \(rect.perimeter())") // Perimeter: 30.0Mutating Methods
Methods that modify struct properties must be marked as mutating:
struct Counter { var count = 0
mutating func increment() { count += 1 }
mutating func increment(by amount: Int) { count += amount }
mutating func reset() { count = 0 }}
var counter = Counter()print(counter.count) // 0
counter.increment()print(counter.count) // 1
counter.increment(by: 5)print(counter.count) // 6
counter.reset()print(counter.count) // 0Defining Classes
Basic Class
class Vehicle { var brand: String var year: Int
init(brand: String, year: Int) { self.brand = brand self.year = year }
func describe() { print("\(year) \(brand)") }}
let car = Vehicle(brand: "Toyota", year: 2022)car.describe() // Output: 2022 ToyotaClass with Computed Properties
class Circle { var radius: Double
var diameter: Double { return radius * 2 }
var area: Double { return Double.pi * radius * radius }
var circumference: Double { return 2 * Double.pi * radius }
init(radius: Double) { self.radius = radius }}
let circle = Circle(radius: 5)print("Radius: \(circle.radius)") // 5.0print("Diameter: \(circle.diameter)") // 10.0print("Area: \(circle.area)") // ~78.54print("Circumference: \(circle.circumference)") // ~31.42Properties
Stored Properties
Store constant or variable values:
struct Point { var x: Double var y: Double}
class Temperature { var celsius: Double let unit: String = "°C" // Constant property
init(celsius: Double) { self.celsius = celsius }}
var point = Point(x: 10, y: 20)print("Point: (\(point.x), \(point.y))")
let temp = Temperature(celsius: 25)print("\(temp.celsius)\(temp.unit)") // 25.0°CComputed Properties
Calculate values rather than storing them:
struct Rectangle { var width: Double var height: Double
var area: Double { return width * height }
var perimeter: Double { return 2 * (width + height) }}
let rect = Rectangle(width: 10, height: 5)print("Area: \(rect.area)") // 50.0Get and Set
Computed properties can have getters and setters:
struct Temperature { var celsius: Double
var fahrenheit: Double { get { return celsius * 9/5 + 32 } set { celsius = (newValue - 32) * 5/9 } }}
var temp = Temperature(celsius: 0)print("Celsius: \(temp.celsius)") // 0.0print("Fahrenheit: \(temp.fahrenheit)") // 32.0
temp.fahrenheit = 212print("Celsius: \(temp.celsius)") // 100.0Property Observers
Respond to changes in property values:
class BankAccount { var balance: Double = 0 { willSet { print("About to set balance to \(newValue)") } didSet { print("Balance changed from \(oldValue) to \(balance)") if balance < 0 { print("⚠️ Account overdrawn!") } } }}
let account = BankAccount()account.balance = 100// Output:// About to set balance to 100.0// Balance changed from 0.0 to 100.0
account.balance = -50// Output:// About to set balance to -50.0// Balance changed from 100.0 to -50.0// ⚠️ Account overdrawn!Lazy Properties
Initialized only when first accessed:
class DataManager { lazy var data: [String] = { print("Loading data...") return ["Item 1", "Item 2", "Item 3"] }()
init() { print("DataManager initialized") }}
let manager = DataManager()// Output: DataManager initialized
print("Accessing data...")print(manager.data[0])// Output:// Accessing data...// Loading data...// Item 1
print(manager.data[1])// Output: Item 2 (data already loaded)Type Properties
Belong to the type itself, not instances:
struct Math { static let pi = 3.14159 static var computationCount = 0
static func square(_ number: Double) -> Double { computationCount += 1 return number * number }}
print(Math.pi) // 3.14159print(Math.square(5)) // 25.0print("Computations: \(Math.computationCount)") // 1Methods
Instance Methods
Operate on specific instances:
class Calculator { var result: Double = 0
func add(_ value: Double) { result += value }
func subtract(_ value: Double) { result -= value }
func multiply(by value: Double) { result *= value }
func clear() { result = 0 }}
let calc = Calculator()calc.add(10)calc.multiply(by: 5)print(calc.result) // 50.0Type Methods
Called on the type itself:
struct Converter { static func celsiusToFahrenheit(_ celsius: Double) -> Double { return celsius * 9/5 + 32 }
static func fahrenheitToCelsius(_ fahrenheit: Double) -> Double { return (fahrenheit - 32) * 5/9 }}
print(Converter.celsiusToFahrenheit(25)) // 77.0print(Converter.fahrenheitToCelsius(77)) // 25.0Mutating Methods in Structs
struct Point { var x: Double var y: Double
mutating func moveBy(x deltaX: Double, y deltaY: Double) { x += deltaX y += deltaY }
mutating func reset() { self = Point(x: 0, y: 0) }}
var point = Point(x: 10, y: 20)point.moveBy(x: 5, y: -10)print("Point: (\(point.x), \(point.y))") // (15.0, 10.0)Initializers
Default Initializer
Structs get a memberwise initializer automatically:
struct User { var username: String var email: String}
// Automatic memberwise initializerlet user = User(username: "alice", email: "alice@email.com")Custom Initializers
struct Temperature { var celsius: Double
init(celsius: Double) { self.celsius = celsius }
init(fahrenheit: Double) { self.celsius = (fahrenheit - 32) * 5/9 }
init(kelvin: Double) { self.celsius = kelvin - 273.15 }}
let temp1 = Temperature(celsius: 25)let temp2 = Temperature(fahrenheit: 77)let temp3 = Temperature(kelvin: 298.15)
print(temp1.celsius) // 25.0print(temp2.celsius) // 25.0print(temp3.celsius) // 25.0Initializer Delegation
Initializers can call other initializers:
struct Rectangle { var width: Double var height: Double
init(width: Double, height: Double) { self.width = width self.height = height }
init(square side: Double) { self.init(width: side, height: side) }}
let rect1 = Rectangle(width: 10, height: 5)let rect2 = Rectangle(square: 10) // 10x10 squareFailable Initializers
Return nil if initialization fails:
struct ValidatedAge { var age: Int
init?(age: Int) { guard age >= 0 && age <= 150 else { return nil } self.age = age }}
if let validAge = ValidatedAge(age: 25) { print("Valid age: \(validAge.age)")} else { print("Invalid age")}// Output: Valid age: 25
if let invalidAge = ValidatedAge(age: 200) { print("Valid age: \(invalidAge.age)")} else { print("Invalid age")}// Output: Invalid ageClass Initializers
Classes don’t get automatic memberwise initializers:
class Person { var name: String var age: Int
init(name: String, age: Int) { self.name = name self.age = age }
// Convenience initializer convenience init(name: String) { self.init(name: name, age: 0) }}
let person1 = Person(name: "Alice", age: 25)let person2 = Person(name: "Bob") // age defaults to 0Value Types vs Reference Types
This is the KEY difference between structs and classes!
Structs Are Value Types (Copied)
struct Point { var x: Int var y: Int}
var point1 = Point(x: 10, y: 20)var point2 = point1 // COPY is made
point2.x = 100
print("point1.x: \(point1.x)") // 10 (unchanged)print("point2.x: \(point2.x)") // 100 (changed)Classes Are Reference Types (Shared)
class Person { var name: String var age: Int
init(name: String, age: Int) { self.name = name self.age = age }}
let person1 = Person(name: "Alice", age: 25)let person2 = person1 // SAME instance (reference)
person2.age = 30
print("person1.age: \(person1.age)") // 30 (changed!)print("person2.age: \(person2.age)") // 30 (changed!)Identity Operators
Check if two references point to the same instance:
class Book { var title: String init(title: String) { self.title = title }}
let book1 = Book(title: "Swift Guide")let book2 = book1let book3 = Book(title: "Swift Guide")
// Check if same instanceif book1 === book2 { print("book1 and book2 are the SAME instance")}// Output: book1 and book2 are the SAME instance
if book1 === book3 { print("Same instance")} else { print("book1 and book3 are DIFFERENT instances")}// Output: book1 and book3 are DIFFERENT instances
// Not identicalif book1 !== book3 { print("Not the same instance")}When to Use Struct vs Class
Use Struct When:
✅ Modeling simple data values
✅ You want value semantics (copying behavior)
✅ The data will be used in multi-threaded code
✅ You don’t need inheritance
Examples: Point, Size, Rectangle, Color, Coordinate
Use Class When:
✅ You need inheritance
✅ You need reference semantics (sharing behavior)
✅ You need to deinitialize resources
✅ Representing a single, shared resource
Examples: ViewController, NetworkManager, Database, FileManager
Practical Examples
Example 1: User Profile (Struct)
struct UserProfile { var username: String var email: String var avatarURL: String? var followers: Int = 0
var displayName: String { return "@\(username)" }
mutating func follow() { followers += 1 }
mutating func updateEmail(_ newEmail: String) { email = newEmail }}
var profile = UserProfile(username: "alice", email: "alice@email.com")print(profile.displayName) // @alice
profile.follow()print("Followers: \(profile.followers)") // 1Example 2: Bank Account (Class)
class BankAccount { let accountNumber: String private(set) var balance: Double var owner: String
init(accountNumber: String, owner: String, initialBalance: Double = 0) { self.accountNumber = accountNumber self.owner = owner self.balance = initialBalance }
func deposit(_ amount: Double) { guard amount > 0 else { return } balance += amount print("Deposited $\(amount). New balance: $\(balance)") }
func withdraw(_ amount: Double) -> Bool { guard amount > 0 && amount <= balance else { print("Insufficient funds") return false } balance -= amount print("Withdrew $\(amount). New balance: $\(balance)") return true }}
let account = BankAccount(accountNumber: "123456", owner: "Alice", initialBalance: 1000)account.deposit(500) // Deposited $500. New balance: $1500account.withdraw(200) // Withdrew $200. New balance: $1300Example 3: Task List (Mixed)
struct Task { var title: String var isCompleted: Bool = false var priority: Priority
enum Priority: String { case low = "Low" case medium = "Medium" case high = "High" }
mutating func complete() { isCompleted = true }}
class TaskManager { private var tasks: [Task] = []
func addTask(_ task: Task) { tasks.append(task) print("Added task: \(task.title)") }
func completeTask(at index: Int) { guard tasks.indices.contains(index) else { return } tasks[index].complete() print("Completed: \(tasks[index].title)") }
func listTasks() { for (index, task) in tasks.enumerated() { let status = task.isCompleted ? "✓" : "○" print("\(index + 1). \(status) \(task.title) [\(task.priority.rawValue)]") } }}
let manager = TaskManager()manager.addTask(Task(title: "Learn Swift", priority: .high))manager.addTask(Task(title: "Build app", priority: .medium))manager.listTasks()// Output:// 1. ○ Learn Swift [High]// 2. ○ Build app [Medium]
manager.completeTask(at: 0)manager.listTasks()// Output:// Completed: Learn Swift// 1. ✓ Learn Swift [High]// 2. ○ Build app [Medium]Example 4: Shopping Cart
struct Product { let id: String let name: String let price: Double}
struct CartItem { let product: Product var quantity: Int
var total: Double { return product.price * Double(quantity) }}
class ShoppingCart { private var items: [CartItem] = []
func addItem(_ product: Product, quantity: Int = 1) { if let index = items.firstIndex(where: { $0.product.id == product.id }) { items[index].quantity += quantity } else { items.append(CartItem(product: product, quantity: quantity)) } print("Added \(quantity) × \(product.name)") }
func removeItem(productId: String) { items.removeAll { $0.product.id == productId } }
func total() -> Double { return items.reduce(0) { $0 + $1.total } }
func checkout() { print("\n--- Receipt ---") for item in items { print("\(item.quantity) × \(item.product.name): $\(item.total)") } print("--------------") print("Total: $\(total())") items.removeAll() }}
let cart = ShoppingCart()cart.addItem(Product(id: "1", name: "iPhone", price: 999.99), quantity: 1)cart.addItem(Product(id: "2", name: "AirPods", price: 179.99), quantity: 2)cart.checkout()Example 5: Game Character
class Character { var name: String var health: Int var maxHealth: Int var level: Int = 1
var isAlive: Bool { return health > 0 }
init(name: String, health: Int) { self.name = name self.health = health self.maxHealth = health }
func takeDamage(_ amount: Int) { health = max(0, health - amount) print("\(name) took \(amount) damage. Health: \(health)/\(maxHealth)")
if !isAlive { print("\(name) has been defeated!") } }
func heal(_ amount: Int) { health = min(maxHealth, health + amount) print("\(name) healed \(amount). Health: \(health)/\(maxHealth)") }
func levelUp() { level += 1 maxHealth += 20 health = maxHealth print("\(name) leveled up to \(level)! Max health: \(maxHealth)") }}
let hero = Character(name: "Warrior", health: 100)hero.takeDamage(30) // Warrior took 30 damage. Health: 70/100hero.heal(20) // Warrior healed 20. Health: 90/100hero.levelUp() // Warrior leveled up to 2! Max health: 120Best Practices
1. Prefer Structs by Default
// ✅ Use struct for simple data modelsstruct Settings { var theme: String var fontSize: Int var notifications: Bool}
// ✅ Use class when you need reference semanticsclass DatabaseConnection { // Shared resource}2. Use Private for Implementation Details
class Counter { private var count = 0 // Hidden from outside
func increment() { count += 1 }
func getCount() -> Int { return count }}3. Use Computed Properties for Derived Values
struct Circle { var radius: Double
// ✅ Computed property var area: Double { return Double.pi * radius * radius }
// ❌ Don't store derived values // var area: Double}4. Initialize All Properties
// ✅ All properties initializedstruct Person { var name: String var age: Int
init(name: String, age: Int) { self.name = name self.age = age }}
// ❌ Don't leave properties uninitialized// class BadPerson {// var name: String // Error!// }5. Use Lazy for Expensive Operations
class DataProcessor { lazy var expensiveData: [String] = { // Only computed when first accessed return loadExpensiveData() }()
private func loadExpensiveData() -> [String] { // Expensive operation return ["Data"] }}Common Mistakes to Avoid
1. Modifying Struct Without mutating
struct Point { var x: Int var y: Int
// ❌ Error - needs mutating // func move() { // x += 1 // }
// ✅ Correct mutating func move() { x += 1 }}2. Unexpected Copying with Structs
struct Array<T> { // Copying can be expensive for large arrays}
// ❌ Unnecessary copyfunc process(_ array: [Int]) { // Creates a copy}
// ✅ Use inout to avoid copyfunc process(_ array: inout [Int]) { // Modifies original}3. Using Class When Struct Would Work
// ❌ Unnecessary classclass Point { var x: Int var y: Int
init(x: Int, y: Int) { self.x = x self.y = y }}
// ✅ Struct is better herestruct Point { var x: Int var y: Int}Summary
Structures and classes are fundamental to Swift programming:
Structures 📦
- Value types (copied)
- No inheritance
- Memberwise initializer
- Thread-safe by default
- Prefer for simple data
Classes 🏛️
- Reference types (shared)
- Support inheritance
- Manual initializers required
- Reference counting
- Use for complex shared resources
Properties 🎯
- Stored (hold values)
- Computed (calculate values)
- Observers (willSet, didSet)
- Lazy (delayed initialization)
- Type properties (static)
Methods ⚙️
- Instance methods
- Type methods (static)
- Mutating (for structs)
Initializers 🚀
- Default and custom
- Memberwise (structs)
- Failable (return nil)
- Convenience initializers
Next Steps
Congratulations on mastering structures and classes! 🎉
Tomorrow, we’ll explore:
- Topic 14: Properties
- Stored properties
- Computed properties
- Property observers (willSet, didSet)
- Type properties (static)
- Lazy properties
Practice Exercises
Try these to reinforce your learning:
- Create a
Bookstruct with computed properties for price with tax - Build a
Studentclass with methods to add grades and calculate average - Create a
Stackstruct with push, pop, and peek methods - Implement a
Timerclass with start, stop, and reset functionality - Build a
Pizzastruct with size, toppings, and price calculation - Create a
Libraryclass that manages books (add, remove, search) - Implement a value vs reference type demonstration program
Master structs and classes to build powerful, organized code! 🏗️
Remember: Use structs by default, classes when you need them!