Weak let for Safer Immutable References


Greetings, traveler!

Swift 6.2 brings a subtle but powerful language enhancement: you can now declare properties using weak let. This complements the existing weak var, offering a new way to create immutable yet non-owning references to class instances.

At first glance, weak let might seem like a contradiction — how can something be both weak and immutable? But this combination unlocks new opportunities for safer architecture and memory management, particularly in concurrency and Sendable contexts.

What is weak let?

With weak let, you can create a reference that:

  • Does not retain the referenced object (like all weak references),
  • Cannot be reassigned after initialization (unlike weak var).

This means the reference can still become nil if the object is deallocated, but you can’t point it to something else later.

Example

Consider a scenario where you have a Downloader class that communicates progress to its delegate. Traditionally, the delegate must be a weak var to avoid a retain cycle — especially when the delegate is a view controller that owns the downloader.

With Swift 6.2, we can now use weak let for delegates if they are set once and never change, making the relationship safer and compatible with Sendable.

protocol DownloaderDelegate: AnyObject {
    func downloadDidUpdate(progress: Double)
}

final class Downloader: Sendable {
    weak let delegate: DownloaderDelegate?

    init(delegate: DownloaderDelegate?) {
        self.delegate = delegate
    }

    func simulateDownload() {
        // Simulated update
        delegate?.downloadDidUpdate(progress: 0.5)
    }
}

var controller: ViewController? = ViewController()
let downloader = Downloader(delegate: controller)
downloader.simulateDownload()

controller = nil
downloader.simulateDownload() // delegate is now nil, no message sent
downloader.delegate = AnotherViewController() // ❌ compiler error

And here’s how we’d use it:

final class ViewController: DownloaderDelegate {
    func downloadDidUpdate(progress: Double) {
        print("Progress: \(progress)")
    }
}

var controller: ViewController? = ViewController()
let downloader = Downloader(delegate: controller)
downloader.simulateDownload()

controller = nil
downloader.simulateDownload() // delegate is now nil, no message sent
downloader.delegate = AnotherViewController() // ❌ compiler error

Why Is This Useful?

Initially, it may not be obvious why weak let is needed. If you can’t reassign it, why not just use weak var?

Here’s why it matters:

  • Sendable compliance: weak var can’t be marked Sendable, but weak let can. That makes weak let especially useful in Swift’s structured concurrency.
  • Immutable design: You can now hold weak references in value types or concurrency-safe contexts without compromising immutability.
  • Cleaner ownership graphs: weak let helps reduce strong reference cycles while preserving thread safety and intent.

When to Reach for weak let

  • In actor-isolated or Sendable types that still need weak references.
  • When working with UI references that must not retain their targets, like views pointing to their view models.
  • For creating snapshot-like relationships that should automatically break when the source disappears.

Conclusion

weak let may not be a feature you reach for daily, but it’s a perfect example of Swift evolving toward more expressive, safer code — especially in modern, concurrent architectures.