Skip to Content
Course content

23: Strong, Weak, and Unowned References

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.