Retain Cycle when annotating member with @ObservedObject in SubView used as destination in NavigationLink

Viewed 235

When using an @ObservedObject member inside a SubView that is used as the destination of a NavigationLink, that class is never deallocated.

Example:

import Combine
import SwiftUI

struct ParentView: View {
    
    var body: some View {
        NavigationView {
            List {
                ForEach(0..<10) { i in
                    ZStack {
                        Text("row \(i)")
                        NavigationLink(destination: SubView()) {
                            EmptyView()
                        }
                    }
                }
            }
        }
    }
}

struct SubView: View {
    @ObservedObject private var viewModel = ViewModel()
    
    var body: some View {
        Text("bar")
    }
}

final class ViewModel: ObservableObject {
    
    init() {
        print("init - \(String(describing: type(of: self)))")
    }
    
    deinit {
        print("deinit - \(String(describing: type(of: self)))")
    }
}

Going back and forth the navigation stack will produce as many instances of ViewModel in this case.

Memory Graph: enter image description here

1 Answers

In iOS 14 this can be fixed by annotating viewModel with @StateObject instead.

When using @ObservedObject you'll notice that ViewModel is initialised when constructing the ParentView. When using @StateObject ViewModel is initialised only once you navigate to SubView.


In iOS 13 you will most likely have to have Bindings in your SubView or use an @EnvironmentObject that is shared between all your SubViews to break the retain cycle since @StateObject was introduced in iOS 14.


What is the reason?

I'm assuming Apple introduced @StateObject to address this problem and to make it clear to the compiler that the class member should be deallocated when the owning struct is removed from memory.

Related