New in Swift 6.4 (which at the time of writing is in beta as part of Xcode 27) is a warning which is emitted when a nested closure performs a weak capture while the parent closure implicitly captures that object as a strong reference.

At first glance, that new diagnostic could be a bit puzzling, since it might seem unnecessary to have to specify such a weak capture twice (or, alternatively, move it to the parent closure’s capture list), but it turns out that it’s an incredibly useful new warning that can help us avoid a quite common kind of memory management bug.

Take the following code as an example. It performs a nested weak self capture within the closure passed to the call to loadNearbyFriends, while not (explicitly) capturing self at all within the parent closure:

@MainActor final class NearbyFriendsViewModel {
    private(set) var friends = [Friend]()

    private var timer: Timer?
    private let service: FriendsService
    
    ...

    @MainActor deinit {
        timer?.invalidate()
    }

    func startScanning() {
        timer = Timer.scheduledTimer(
            withTimeInterval: 3,
            repeats: true,
            block: { [service] _ in
                service.loadNearbyFriends { [weak self] friends in
                    self?.friends = friends
                }
            }
        )
    }
}

When using Xcode 26 and Swift 6.3, the above code is successfully compiled without any warnings. However, it’s actually causing our NearbyFriendsViewModel to be trapped in a retain cycle, which might seem quite strange given that we haven’t performed any strong captures of self — at least not explicitly.

However, it turns out that when we perform a weak self capture within a nested closure (like we do above), then the Swift compiler implicitly inserts a strong self capture within the parent closure unless it performs an explicit capture of its own. Within the above NearbyFriendsViewModel example, that means that our Timer actually captures self, which in turn retains that object through its timer property, and there we go — a classic retain cycle.

If we instead switch to Xcode 27 and Swift 6.4, then we can see that we now actually get a warning when attempting to compile the above code:

'weak' ownership of capture 'self' differs from implicitly-captured
strong reference in outer scope

That’s great, because it clearly shows us where the problem is. So, let’s go ahead and fix it. In this specific case, since we’re not actually using self within the outer closure, we can simply move our weak self capture to that closure instead:

@MainActor final class NearbyFriendsViewModel {
    ...

    func startScanning() {
        timer = Timer.scheduledTimer(
            withTimeInterval: 3,
            repeats: true,
            block: { [weak self, service] _ in
                service.loadNearbyFriends { friends in
                    self?.friends = friends
                }
            }
        )
    }
}

That successfully breaks our retain cycle, and resolves the warning that the Swift 6.4 compiler previously gave us.

Another option would be to perform two separate weak self captures, which would be a good solution if our outer closure actually needed to use self in any way, such as in this case:

@MainActor final class NearbyFriendsViewModel {
    ...

    func startScanning() {
        timer = Timer.scheduledTimer(
            withTimeInterval: 3,
            repeats: true,
            block: { [weak self] _ in
                guard let self else { return }

                logScanningDidStart()

                service.loadNearbyFriends { [weak self] friends in
                    self?.friends = friends
                }
            }
        )
    }
    
    private nonisolated func logScanningDidStart() {
        ...
    }
}

Finally, in situations where capturing self strongly doesn’t actually lead to a retain cycle, we can silence the new warning for nested weak captures by explicitly adding self to the outer closure’s capture list, such as when creating a Task within the following example:

@MainActor final class NearbyFriendsViewModel {
    ...
    private let cache: FriendsCache
    ...

    func reloadIfNeeded() {
        Task { [self] in
            guard await !cache.isValid else { return }

            service.loadNearbyFriends { [weak self] friends in
                self?.friends = friends
            }
        }
    }
}

Above it’s perfectly fine to capture self strongly within our task’s closure, since once the cache check has been performed and our call to loadNearbyFriends has been dispatched, then that closure will be deallocated.

It’s great to see new diagnostics added to Swift that causes warnings to be omitted for common mistakes, anti-patterns and potential memory issues. Although such diagnostics might require us to have to slightly over-specify our code in certain places, requiring us to be explicit about potentially problematic self captures is definitely a good thing, since it’s such a common source of retain cycles and other memory management bugs.

Thanks for reading!