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?
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.
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.