Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
22: Automatic Reference Counting Explained
A few years ago, I was reviewing a PR for a junior dev who was building a music streaming app. Everything looked clean, but during the QA phase, we noticed a weird bug: every time a user switched songs, the app's memory usage crept up slightly. It wasn't a massive jump, but after thirty minutes of listening, the app would simply vanish—a classic OOM (Out of Memory) crash. When we looked at the memory graph, we saw hundreds of SongPlayer and Playlist objects that should have been destroyed minutes ago, but they were still hanging around in memory like ghosts. They were trapped in a "strong reference cycle," and because they were holding onto each other, Swift's memory manager couldn't kill either of them.
How Swift Tracks Your Objects
In Swift, classes are reference types. This means when you assign a class instance to a variable, you aren't making a copy of the data; you're just creating another pointer to the same spot in memory. To keep track of when it's safe to delete that memory, Swift uses Automatic Reference Counting, or ARC.
Think of ARC as a tally system. Every time you create a strong reference to a class instance, Swift increments a counter. When that reference goes out of scope or is set to nil, the counter decrements. Once that counter hits zero, Swift knows that nobody is using that object anymore, and it immediately deallocates the memory. It's incredibly efficient because it happens in real-time, unlike the "Garbage Collection" you see in languages like Java or C#, which pauses the program to clean up in batches.
The Death Grip of Strong Reference Cycles
Most of the time, ARC just works. You don't even think about it. But you'll run into trouble the moment two class instances hold strong references to each other. This is the "death grip" I mentioned earlier.
Imagine a Tenant class and an Apartment class. The tenant has a property for their apartment, and the apartment has a property for its tenant. If both are declared as standard properties, they both hold "strong" references. Even if the rest of your app forgets about the tenant and the apartment, they are still holding onto each other. Their reference counts will never hit zero, and they will leak memory until the OS kills your process.
class Tenant {
let name: String
var apartment: Apartment?
init(name: String) { self.name = name }
deinit { print("\(name) is being deallocated") }
}
class Apartment {
let unit: String
var tenant: Tenant?
init(unit: String) { self.unit = unit }
deinit { print("Apartment \(unit) is being deallocated") }
}
var john: Tenant? = Tenant(name: "John")
var unit4B: Apartment? = Apartment(unit: "4B")
john?.apartment = unit4B
unit4B?.tenant = john
// Now, even if we do this...
john = nil
unit4B = nil
// Nothing is printed. The memory is leaked.
Breaking the Cycle with Weak and Unowned
To fix this, you have to tell Swift that one of these references shouldn't keep the object alive. You do this using the weak or unowned keywords. A weak reference does not increment the reference count. Because it doesn't guarantee the object will stay alive, Swift forces weak variables to be optionals. If the object it points to is deallocated, the weak variable automatically becomes nil.
Then there is unowned. It's similar to weak in that it doesn't increment the count, but it's non-optional. You use unowned only when you are 100% certain that the other object will exist for as long as the current object does. If you try to access an unowned reference after the object is gone, your app will crash. I usually lean toward weak unless I have a very specific architectural reason to use unowned—crashing is generally worse than dealing with an optional.
In the example above, making the tenant property in the Apartment class weak would solve everything. When john is set to nil, the Tenant instance's count drops to zero. It gets deallocated, which in turn drops the reference count of the Apartment to zero, and both are cleaned up perfectly.
📋 Practical Task
Fixing the Memory Leak in a Customer-Order System
You are working on an e-commerce backend. You've noticed that Customer and Order objects are staying in memory long after the checkout process is finished. Below is the problematic code. Your task is to modify the classes so that when a customer is set to nil, both the customer and their order are properly deallocated from memory.
class Customer {
let name: String
var currentOrder: Order?
init(name: String) {
self.name = name
}
deinit {
print("Customer \(name) deleted")
}
}
class Order {
let orderId: Int
var customer: Customer?
init(orderId: Int) {
self.orderId = orderId
}
deinit {
print("Order \(orderId) deleted")
}
}
// TEST CASE
var activeCustomer: Customer? = Customer(name: "Alice")
var activeOrder: Order? = Order(orderId: 12345)
activeCustomer?.currentOrder = activeOrder
activeOrder?.customer = activeCustomer
print("Setting references to nil...")
activeCustomer = nil
activeOrder = nil
// Currently, nothing prints. Fix the classes so that both deinit messages appear.There are no comments for now.