1

I previously asked a question about how to push a view with data received from an asynchronous callback. The method I ended up with has turned out to cause a Memory Leak.

I'm trying to structure my app with MVVM for SwiftUI, so a ViewModel should publish another ViewModel, that a View then knows how to present on screen. Once the presented view is dismissed from screen, I expect the corresponding ViewModel to be deinitialised. However, that's never the case with the proposed solution.

After UserView is dismissed, I end up having an instance of UserViewModel leaked in memory. UserViewModel never prints "Deinit UserViewModel", at least not until next time a view is pushed on pushUser.

struct ParentView: View {
    @ObservedObject var vm: ParentViewModel
    
    var presentationBinding: Binding<Bool> {
        .init(get: { vm.pushUser != nil },
              set: { isPresented in
                if !isPresented {
                    vm.pushUser = nil
                }
              }
        )
    }
    
    var body: some View {
        VStack {
            Button("Get user") {
                vm.getUser()
            }
            Button("Read user") {
                print(vm.pushUser ?? "No userVm")
            }
            if let userVm = vm.pushUser {
                NavigationLink(
                    destination: UserView(vm: userVm),
                    isActive: presentationBinding,
                    label: EmptyView.init
                )
            }
        }
    }
}

class ParentViewModel: ObservableObject {
    @Published var pushUser: UserViewModel? = nil
    
    var cancellable: AnyCancellable?
    
    private func fetchUser() -> AnyPublisher<User, Never> {
        Just(User.init(id: "1", name: "wiingaard"))
            .delay(for: .seconds(1), scheduler: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
    
    func getUser() {
        cancellable = api.getUser().sink { [weak self] user in
            self?.pushUser = UserViewModel(user: user)
        }
    }
}

struct User: Identifiable {
    let id: String
    let name: String
}

class UserViewModel: ObservableObject, Identifiable {
    deinit { print("Deinit UserViewModel") }

    @Published var user: User
    init(user: User) { self.user = user }
}

struct UserView: View {
    @ObservedObject var vm: UserViewModel    
    var body: some View {
        Text(vm.user.name)
    }
}

After dismissing the UserView and I inspect the Debug Memory Graph, I see an instance of UserViewModel still allocated.

Leaked Object from Xcode's memory debugger

The top reference (view.content.vm) has kind: (AnyViewStorage in $7fff57ab1a78)<ModifiedContent<UserView, (RelationshipModifier in $7fff57ad2760)<String>>> and hierarchy: SwiftUI.(AnyViewStorage in $7fff57ab1a78)<SwiftUI.ModifiedContent<MyApp.UserView, SwiftUI.(RelationshipModifier in $7fff57ad2760)<Swift.String>>> AnyViewStorageBase _TtCs12_SwiftObject

What's causing this memory leak, and how can I remove it?

Wiingaard
  • 4,150
  • 4
  • 35
  • 67

2 Answers2

0

I can see that ViewModel is deinit() if you use @State in your View, and listen to your @Publisher in your ViewModel.

Example:

@State var showTest = false

 NavigationLink(destination: SessionView(sessionViewModel: outgoingCallViewModel.sessionViewModel),
                           isActive: $showTest,
                           label: { })
                .isDetailLink(false)
        )
        .onReceive(viewModel.$showView, perform: { show in
            if show {
                showTest = true
            }
        })

if you use viewModel.$show in your NavigationLink as isActive, viewModel never deinit().

zdravko zdravkin
  • 2,090
  • 19
  • 21
-1

Please refer to this post (https://stackoverflow.com/a/62511130/11529487), it solved the issue for the memory leak bug in SwiftUI by adding on the NavigationView:

.navigationViewStyle(StackNavigationViewStyle())

However it breaks the animation, there is a hacky solution to this issue. The animation problem occurs because of the optional chaining in "if let".

When setting "nil" as the destination in the NavigationLink, it essentially does not go anywhere, even if the "presentationBinding" is true.

I invite you to try this piece of code as it fixed the animtaion problem that resulted from the StackNavigationViewStyle (and no memory leaks):

Hacky solution to animation problem.

Although not as pretty as the optional chaining, it does the job.

  • 1
    Ohh wow, I was not aware that you can pass `nil` to destination.. This solved my issues, thanks I've also updated my `push` ViewModifier [here](https://stackoverflow.com/a/66500771/2299801) – Wiingaard Mar 09 '21 at 19:39
  • I am glad this solved your issue! Together in this community we can solve SwiftUI's simple issues like this until it becomes mature in the near future (hopefully ) – Abdulelah Hajjar Mar 10 '21 at 14:17