I'm determined to fully understand why this isn't causing a reference cycle. And in general what is happening at each stage of memory management here.
I have the following setup:
struct PresenterView: View {
@State private var isPresented = false
var body: some View {
Text("Show")
.sheet(isPresented: $isPresented) {
DataList()
}
.onTapGesture {
isPresented = true
}
}
}
struct DataList: View {
@StateObject private var viewModel = DataListViewModel()
var body: some View {
NavigationView {
List(viewModel.itemViewModels, id: \.self) { itemViewModel in
Text(itemViewModel.displayText)
}.onAppear {
viewModel.fetchData()
}.navigationBarTitle("Items")
}
}
}
class DataListViewModel: ObservableObject {
private let webService = WebService()
@Published var itemViewModels = [ItemViewModel]()
private var cancellable: AnyCancellable?
func fetchData() {
cancellable = webService.fetchData().sink(receiveCompletion: { _ in
//...
}, receiveValue: { dataContainer in
self.itemViewModels = dataContainer.data.items.map { ItemViewModel($0) }
})
}
deinit {
print("deinit")
}
}
final class WebService {
var components: URLComponents {
//...
return components
}
func fetchData() -> AnyPublisher<DataContainer, Error> {
return URLSession.shared.dataTaskPublisher(for: components.url!)
.map { $0.data }
.decode(type: DataContainer.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
So when I create a PresenterView and then dismiss it I get a successful deinit print.
However I don't understand why they is no reference cycle here. DataListViewModel
has cancellables
which has a subscription that captures self. So DataListViewModel
-> subscription and subscription -> DataListViewModel
. How can deinit
be triggered? In general is there a good approach to understanding whether there is a retain cycle in these kinds of situation?
The closure, as you expected, does retain a strong reference to self
. The closure itself is maintained by the Sink
subscriber.
If nothing else happens, this is a memory leak because the subscriber is never cancelled, because AnyCancellable
is never released, because self
never de-inits, and self
never de-inits because the subscriber is holding a reference it.
However, in your case, the publisher completes, and that's another way for the subscriber to release its closures. So, self
is only released after the pipeline completes.
To illustrate, we can use a PassthroughSubject
to explicitly send a completion:
class Foo {
var c: AnyCancellable? = nil
func fetch() {
let subject = PassthroughSubject<String, Never>()
c = subject.sink {
self.c // capture self
print($0)
}
subject.send("sync")
DispatchQueue.main.async { subject.send("async") }
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
subject.send("async 2 sec")
subject.send(completion: .finished)
}
}
deinit { print("deinit") }
}
do {
Foo().fetch()
}
Because self
is captured, it's not released until after a completion is sent 2 seconds later:
sync
async
async 2 sec
deinit
If you comment out the line subject.send(completion: .finished)
, there will not be a deinit
:
sync
async
async 2 sec
If you use [weak self]
in the closure, the pipeline would cancel:
sync
deinit