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
24: Avoiding Retain Cycles
I was working on a small game project the other day, and I noticed something weird. Even after I navigated away from the game screen and dismissed the controller, the memory usage in Xcode's Debug Navigator stayed flat instead of dropping. In a small app, you might not notice, but in a professional production app, this is how you get those dreaded "Out of Memory" crashes.
The Memory Leak we didn't see coming
I decided to isolate the problem. I had a GameManager that handled the score and a Player object that represented the user. To make it work, the manager needed to know who the player was, and the player needed a reference back to the manager to report when they picked up a coin. It seems intuitive, right? Let's look at how I first wrote it.
class GameManager {
var player: Player?
deinit {
print("GameManager is being deallocated!")
}
}
class Player {
var manager: GameManager?
deinit {
print("Player is being deallocated!")
}
}
Now, here is where I tried to run a quick test in a playground to see if these objects were actually dying when I was done with them.
var manager: GameManager? = GameManager()
var player: Player? = Player()
manager?.player = player
player?.manager = manager
// Now I'm done with them. I'll set them to nil.
manager = nil
player = nil
I expected to see both "deallocated" messages in the console. Instead... nothing. Complete silence. This is the classic "Retain Cycle." Because the manager has a strong hold on the player, and the player has a strong hold on the manager, they are essentially keeping each other alive in a death grip. Even though I told my local variables to be nil, the two objects are still pointing at each other in memory, so Swift's Automatic Reference Counting (ARC) can't reclaim them.
Breaking the loop with weak
To fix this, I have to decide who "owns" whom. In this relationship, the GameManager is the boss; it creates and manages the Player. The player just needs to be able to talk back to the boss, but it shouldn't be responsible for keeping the boss alive.
I'll change the reference in the Player class to be weak. A weak reference doesn't increase the reference count of the object it points to. It's like saying, "I know where you are, but I'm not holding onto you."
class Player {
// Added 'weak' here
weak var manager: GameManager?
deinit {
print("Player is being deallocated!")
}
}
If I run that same test code again, the magic happens. The moment I set manager = nil, the GameManager is deallocated. Since it was the only thing holding a strong reference to the Player, the player is then deallocated immediately after. The console finally prints both messages.
The "unowned" alternative
You might wonder why we use weak, which requires the variable to be an optional (since the object could disappear while we're still looking at it). Sometimes, you know for a fact that the child object will never outlive the parent. In those cases, you can use unowned.
unowned is similar to weak in that it doesn't increase the reference count, but it assumes the reference will always be there. It's not an optional. However, I'll give you a piece of professional advice: be careful with it. If you access an unowned reference after the object it points to has been deleted, your app will crash instantly. When in doubt, just use weak. The safety of an optional is usually worth the extra ? in your code.
📋 Practical Task
Fixing the Social Media Profile Leak
You are reviewing a teammate's code for a social media app. They've created a UserProfile class and a ProfileSettings class. The UserProfile owns the ProfileSettings, but the settings object needs a reference back to the user to update the database. Currently, this is causing a retain cycle.
Your Goal: Modify the provided code to break the retain cycle so that both objects are properly deallocated when the user logs out.
class UserProfile {
let username: String
var settings: ProfileSettings?
init(username: String) {
self.username = username
}
deinit {
print("UserProfile for \(username) deleted")
}
}
class ProfileSettings {
var user: UserProfile?
deinit {
print("ProfileSettings deleted")
}
}
// Test setup
var currentUser: UserProfile? = UserProfile(username: "SwiftDev99")
var currentSettings: ProfileSettings? = ProfileSettings()
currentUser?.settings = currentSettings
currentSettings?.user = currentUser
// This should trigger deinit for both, but currently doesn't
currentUser = nil
currentSettings = nil
Update the ProfileSettings class to use the correct reference type to ensure the memory is freed.
There are no comments for now.