[weak self] is not always the solution

Publish date: March 14, 2021

Tags: swift, ios, tips and tricks

At a certain point in your iOS development journey, you probably encountered a retain cycle when working with closures, which you eventually solves by using [weak self] in the capture list.

If this happens often enough, you might adopt a blanket rule such as:

All closures must have [weak self]!

Yes, this will solve your retain cycles, but it can introduce problems that are worse.

This happened very often with my colleagues, particularly when they are learning reactive frameworks, like RxSwift or Combine, since those make extensive use of closures.

Problem

Suppose you are building a social media app, and you want to locally cache a user’s friends list. When the user adds a new friend, you immediately add it to the cache to provide feedback to the user. In case the network request failed, you revert the change, so the cache has the correct state.

Your code might looks something like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class FriendsListViewController: UIViewController {
    func addFriend(id: String) {
        // Update the local cache immediately for quick user feedback
        localCache.addFriend(with: id)

        // Perform the network request
        apiService.addFriend(id: id, completion: { [weak self] result in
            guard let self = self else { return }
            
            // In case the request fails, revert the changes in the local cache
            if case .failure = result {
                self.localCache.removeFriend(with: id)
            }
        })
    }
}

Can you think of a scenario where this code will not work correctly?

What will happen if the FriendsListViewController gets deallocated while the network request is running? For example, the user dismisses the screen.

When the completion closure is called, self will be nil, so unwrapping will fail and nothing inside the closure will be run.

Solution

You could solve this in two ways:

  1. You could simply capture self strongly, since you know that ApiService does not retain the closure, so self will only be retained until the network request finishes
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class FriendsListViewController: UIViewController {
    func addFriend(id: String) {
        localCache.addFriend(with: id)

        apiService.addFriend(id: id, completion: { result in
            if case .failure = result {
                // Implicitly strongly capture `self`
                self.localCache.removeFriend(with: id)
            }
        })
    }
}
  1. Some people do not know this, but in fact, you can write other things than [weak self] in the capture list! You could strongly capture only localCache in this case.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class FriendsListViewController: UIViewController {
    func addFriend(id: String) {
        localCache.addFriend(with: id)

        apiService.addFriend(id: id, completion: { [localCache] result in
            if case .failure = result {
                // Implicitly strongly capture `self`
                localCache.removeFriend(with: id)
            }
        })
    }
}

This scenario is safe even when apiService retains the completion closure, because there are no circular references. self owns apiService, who owns completion, who owns localCache. But localCache does not own anyone in the chain, so there isn’t a cycle.

Alternatives to [weak self]

1. Capture self strongly

As you’ve seen in the first example, sometimes capturing self strongly is safe.

This can be used in scenarios when:

1
2
3
4
5
6
7
8
9
DispatchQueue.main.async { 
    self.doSomething()
}

UIView.animate(withDuration: 0.5) {
    self.someAnimation()
} completion: { _ in
    self.animationFinished()
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func viewDidLoad() {
    super.viewDidLoad()

    performIfLoggedIn {
        self.doSomethingWhenLoggedIn()
    }
}

// `closure` is not `@escaping`
private func performIfLoggedIn(closure: () -> Void) {
    if isLoggedIn {
        closure()
    }
}

2. Strongly capturing only what you need

You can even have multiple objects in your capture list!

1
2
3
4
5
6
func addFriend(id: id) {
    apiService.addFriend(id: id, completion: { [localCache, otherService] _ in
        localCache.update()
        otherService.notify()
    })
}

However, you need to be careful. The objects in the capture list are read when the closure is created. The above code is identical to:

1
2
3
4
5
6
7
8
9
func addFriend(id: id) {
    let localCache = self.localCache
    let otherService = self.otherService

    apiService.addFriend(id: id, completion: { _ in
        localCache.update()
        otherService.notify()
    })
}

If between the time addFriend is called and the completion closure is called, a new instance is assigned to localCache or otherService, the closure will still hold a reference to the old instance.

Conclusion

In this article, you’ve seen a scenario when using [weak self] can cause problems. And you’ve also seen two alternatives that can be used.

Unfortunately, there is no silver bullet solution that can be applied to all scenarios.

Instead of trying to come up with a blanket rule, I recommend trying to understand how references work and how retain cycles happen. Then stopping and thinking about each scenario to see if a retain cycle is possible and figuring out which option is the best.

I would love to hear your thoughts and suggestions. Feel free to get in touch at moc.htikmsofi@gomlb!