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
23: Strong, Weak, and Unowned References
I remember a junior dev I worked with a few years back who spent three days trying to figure out why their "User Settings" screen was leaking memory. Every time they navigated to the settings page and then backed out, the memory graph in Xcode climbed steadily. They were convinced it was a system bug or a weird UIKit glitch. When we finally sat down to debug it, we found a single closure used to handle a "Save" button. They had captured self strongly inside a closure that was stored in a ViewModel, which was, in turn, owned by the ViewController. It was a classic retain cycle—the objects were essentially holding onto each other in a death grip, and ARC had no way to break the loop.
The Trap of the Strong Reference Cycle
In Swift, everything is handled by Automatic Reference Counting (ARC). Most of the time, it's invisible and works perfectly. But the moment you have two class instances that hold strong references to each other, you've created a retain cycle. Neither object can ever be deallocated because their reference counts will never drop to zero.
Take a look at this common scenario. Imagine a Customer who has a LoyaltyCard, and that card needs to know who its owner is:
class Customer {
let name: String
var loyaltyCard: LoyaltyCard?
init(name: String) { self.name = name }
deinit { print("\(name) is being deallocated") }
}
class LoyaltyCard {
let cardNumber: Int
var owner: Customer?
init(cardNumber: Int) { self.cardNumber = cardNumber }
deinit { print("Card is being deallocated") }
}
If you create a customer and assign them a card, and then assign that customer as the card's owner, you've just built a loop. Even if you set your local variables to nil, those two objects will stay in memory forever. I've seen this kill app performance in production because developers forgot that "strong" is the default.
Breaking the Loop with Weak and Unowned
To fix this, we have to tell Swift that one of these references shouldn't keep the other object alive. We do this using weak or unowned.
A weak reference is the safest bet. It doesn't increase the reference count, and because the object it points to could be deleted while the weak reference still exists, Swift forces it to be an optional. When the object is deallocated, the weak reference automatically becomes nil.
Then there's unowned. It's similar to weak in that it doesn't increase the reference count, but it's non-optional. You use this when you are 100% certain that the other object will outlive the current one. If you're wrong, and you try to access an unowned reference after the object is gone, your app will crash. It's a powerful tool, but I generally advise using weak unless you have a very specific architectural reason to avoid optionals.
In our example, the LoyaltyCard should probably have a weak var owner: Customer?. A card doesn't "own" a human; it's just associated with one.
The Closure Capture List
The most common place you'll encounter these issues isn't between two classes, but between a class and a closure. Since closures are reference types, if a class owns a closure and that closure captures self, you've got a retain cycle.
To stop this, we use a capture list. You'll see this pattern everywhere in professional Swift code:
class DataFetcher {
var onCompletion: ((String) -> Void)?
func startFetch() {
onCompletion = { [weak self] result in
// self is now an optional inside this scope
guard let self = self else { return }
print("Finished fetching \(result) for \(self)")
}
}
}
That [weak self] tells the closure not to hold a strong grip on the class instance. The guard let self = self line—which some people call the "strong-weak dance"—temporarily creates a strong reference for the duration of the closure's execution so you don't have to deal with optionals for every single line of code inside the block.
📋 Practical Task
Fixing the Retain Cycle in a Music Player Manager
You are working on a music app. There is a MusicPlayer class that handles playback, and a Playlist class that manages the queue. Currently, the app is leaking memory every time a playlist is closed because of a strong reference cycle between the player and the playlist.
Your Task: Modify the provided code to break the retain cycle. You must ensure that when the Playlist is set to nil, both the Playlist and the MusicPlayer are properly deallocated (their deinit methods should run).
class MusicPlayer {
var currentPlaylist: Playlist?
deinit {
print("MusicPlayer deallocated")
}
}
class Playlist {
let title: String
var player: MusicPlayer?
var onTrackEnd: ((String) -> Void)?
init(title: String) {
self.title = title
}
func setupPlayer() {
let player = MusicPlayer()
self.player = player
player.currentPlaylist = self
// This closure is the second source of the leak!
self.onTrackEnd = { trackName in
print("Finished playing \(trackName) in \(self.title)")
}
}
deinit {
print("Playlist \(title) deallocated")
}
}
// Testing the leak:
var myPlaylist: Playlist? = Playlist(title: "Chill Vibes")
myPlaylist?.setupPlayer()
myPlaylist = nil // Should trigger deinit for both, but currently doesn't.There are no comments for now.