Why doesn't the example from the book create a retain cycle?

While working through Chapter 3 on protocols we ended up with the following simplified example of the view model:

final class ArticlesViewModel: ObservableObject {
  @Published private(set) var articles: [Article] = []
  private let networker: Networking

  func fetchArticles() {
    let request = ArticleRequest()
    networker.fetch(request)
      .tryMap([Article].init)
      .replaceError(with: [])
      .receive(on: DispatchQueue.main)
      .assign(to: \.articles, on: self)
      .store(in: &cancellables)
  }
}

I was expecting to see a reference cycle between the ArticlesViewModel and Networking class instances. The ArticlesViewModel holds a strong reference to Networking and we pass a strong reference to ArticlesViewModel to the assign method. However, I’ve tested this and there is no retain cycle, ArticlesViewModel gets deallocated as soon as an external reference gets released. What’s missing in my understanding?

Thank you!

Looks like I have found the answer.

From Apple documentation:

Important
The Subscribers.Assign instance created by this operator maintains a strong reference to object, and sets it to nil when the upstream publisher completes (either normally or with an error).

So the closure will keep a strong reference to self until the network request is finished. If we didn’t want to keep the network request around, we could use a sink operator with unowned self in the closure instead. Then the network request will get canceled if the view model gets released.

  func fetchArticles() {
    let request = ArticleRequest()
    networker.fetch(request)
      .delay(for: .seconds(10), scheduler: DispatchQueue.main)
      .tryMap([Article].init)
      .replaceError(with: [])
      .sink { [unowned self] in
        self.articles = $0
      }
      .store(in: &cancellables)
  }
1 Like

The only thing I would change is that you want to use [weak self] instead of [unowned self]. If for some reason the view (self) goes out of scope and the network request completes before cancel happens, [unowned self] would cause your program to trap (crash). It might be that combine cancellation makes it so that scenario is impossible, but that assumption feels a little fragile to me.

Thank you Ray! Yes, I’ve tested this scenario. Combine cancels the request, so sink doesn’t get called when the view model gets released.