Code & Craft by Kasra

Making Weak References Thread-Safe in Swift with a Tiny Macro

If you’ve worked with delegates in Swift, you’ve almost certainly used weak references. They’re the go-to way to avoid retain cycles—but they come with a subtle catch: they’re not thread-safe.

Now, in a lot of cases, that’s fine. If you only ever touch your delegate on the main thread, you’re safe. But what if you want to make your class Sendable and use it across multiple threads in Swift concurrency land? Suddenly, things get messy.

The problem

Here’s a simple example:

import Foundation

final class APINetwork: Sendable {
  // Stored property 'delegate' of 'Sendable'-conforming class 'APINetwork'
  // is mutable; this is an error in the Swift 6 language mode
  weak var delegate: URLSessionDelegate?
}

Even though URLSessionDelegate itself conforms to Sendable, Swift yells at you. Why? Because that weak property is mutable and not thread-safe.

At this point, you might be tempted to slap @MainActor on the class and call it a day. But that’s overkill—especially if your class doesn’t really need to be main-actor isolated. Worse, it forces every call site to also hop to the main actor. Not ideal.

The solution

That’s where Locked Weak Reference comes in. It’s a tiny package I built that lets you wrap all your weak properties in a thread-safe container using a macro. Here’s what your code looks like with it:

import Foundation
import LockedWeakReference

@LockedWeakReference
final class APINetwork: Sendable {
  weak var delegate: URLSessionDelegate?
}

That’s it. No weird boilerplate. No custom locks sprinkled all over your code. The macro finds all weak properties and wraps them in a safe, atomic type under the hood.

If you peek at the expansion, you’ll see it’s doing the lock dance for you—so your weak reference is safe to read and write from multiple threads:

import Foundation
import LockedWeakReference

@LockedWeakReference
final class APINetwork: Sendable {
  @RegisterWeakReference
  weak var delegate: URLSessionDelegate?
  {
    get {
        _delegate.withLock {
            $0 as? URLSessionDelegate
        }
    }
    set {
        _delegate.withLock {
            $0 = newValue
        }
    }
  }
  private let _delegate = LockedWeakReference()
}
private extension APINetwork {
    final class LockedWeakReference: @unchecked Sendable {
        private let lock: NSLock = {
            NSLock()
        }()
        private(set) weak var value: AnyObject?
        func withLock<R>(_ body: (inout AnyObject?) throws -> R) rethrows -> R {
            lock.lock()
            defer {
                lock.unlock()
            }
            return try body(&value)
        }
    }
}

Why this matters

The key benefit here is you can now confidently mark your classes as Sendable without having to mark them @MainActor unnecessarily. That means you avoid cascading main actor constraints to call sites that really don’t need them.

It’s one of those small utilities that scratches a very specific itch—but if you’ve hit this problem, you know how annoying it can be.

Check out the repo here: Locked Weak Reference on GitHub.