Recently I spent several hours troubleshooting and found another case that cause retain cycle without our notice. Here is simplified example from my project.

guard
    let navigationController = self.navigationController
    else { return }

self.rx.contentOffset.subscribe { [weak self] (event) in
    guard let `self` = self else { return }
    
    print(self.description)
    print(navigationController.description)
}.disposed(by: self.rx.disposeBag)

Here navigationController used in the closure is also referenced by self, which create a retain cycle. If you only take care with the obvious self references, it's easy to miss this.

In order to break the retain cycle, declare navigationController as weak reference in the capture list like self.

self.rx.contentOffset.subscribe { [weak self, weak navigationController] (event) in
    guard let `self` = self else { return }
    guard let `navigationController` = navigationController else { return }
    
    print(self.description)
    print(navigationController.description)
}.disposed(by: self.rx.disposeBag)

Every time we use a new variable in a closure, we need to ask ourself: is it a local variable or is it referenced by something else? So that we won't introduce another retain cycle mindlessly. (It's just too easy to create a retain cycle)