Home > front end >  SwiftUI retain cycle in view hierarchy
SwiftUI retain cycle in view hierarchy

Time:02-15

I'm having the following view hierarchy which has a retain cycle, that's the simplest I could make to reproduce the issue. All viewmodels and properties has to stay as they are needed in the original solution:

import SwiftUI

struct MainView: View {
    @StateObject var viewModel = MainViewModel()

    var body: some View {
        NavigationView { [weak viewModel] in
            VStack {
                Button("StartCooking") {
                    viewModel?.show()
                }

                if viewModel?.isShowingContainerView == true {
                    ContainerView()
                }
                Button("StopCooking") {
                    viewModel?.hide()
                }
            }
        }
        .navigationViewStyle(.stack)
    }
}

final class MainViewModel: ObservableObject {
    @Published var isShowingContainerView = false

    func show() {
        isShowingContainerView = true
    }

    func hide() {
        isShowingContainerView = false
    }
}

struct ContainerView: View {
    @Namespace var namespace

    var body: some View {
        VStack {
            SubView(
                namespace: namespace
            )
        }
    }
}


struct SubView: View {
    @StateObject var viewModel = SubViewModel()
    var namespace: Namespace.ID

    var body: some View {
        Text("5 min")
            .matchedGeometryEffect(id: UUID().uuidString, in: namespace)
            .onTapGesture {
                foo()
            }
    }

    private func foo() {}
}


final class SubViewModel: ObservableObject {}

If I run the app, tap on StartCooking, than on StopCooking and check the memory graph, I still see an instance of SubViewModel, which means that there is a leak in this code. If I remove:

  • NavigationView OR
  • The VStack from ContainerView OR
  • matchedGeometryEffect OR
  • tapGesture

The retain cycle is resolved. Unfortunately I need all these. Can you see what the issue might be and how could it be solved?

CodePudding user response:

Looks like a SwiftUI bug. A possible workaround (if sub-view is one or limited set) is to use view model factory to provided instances.

Here is an example for one view:

struct SubView: View {

    @StateObject var viewModel = SubViewModel.shared  // single instance !!

    // .. other code
}

final class SubViewModel: ObservableObject {
    static var shared = SubViewModel()         // << this !!
}

CodePudding user response:

I could kind of workaround it by making every property optional in the SubViewModel and running a function when the SubViews disappear, which makes them nil. The SubViewModel still stays in the memory, but will not take up that much space.

Interestingly I even tried to make the viewmodel optional, and make it nil when the view disappears, but it still stayed in the memory.

  • Related