UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: When does my view model get deinit'ed?

Forums > macOS

I have a view that contains an @StateObject for it's view model. The view model is created as part of the view:

@StateObject var vm: MyViewModel = MyViewModel()

That's fine and the init() function runs to initialise the view model. In my case, I subscribe to a notification message in the init, so I've coded a deinit() function that unsubscribes.

Problem is, the deinit() never seems to run. When I close my view (window), the view model seems to hang around and happily receives notifications and attempts to act on them. What I was expecting was that the view model would go away when the window was closed, but that doesn't appear to be the case.

Is there something else I need to do to get my view model to go away? I'm assuming that, if I had a lot of data sat in the view model, that would also hang around and produce a memory leak. I'm guessing I could code an onDisappear{} in the view to call a reset function in the view model, but that still leaves the view model in existanceand feels a little clunky.

Thanks Steve

3      

I had a play and replaced my notification subscription in the view model (and the deinit) with a .onRecieve in the view itself:

                .onReceive(NotificationCenter.default.publisher(for:
                                                                    Notification.Name(AppNotifications.RefreshAllNotification))) { _ in
                    print("Refresh All Notification")
                }

My thought was that Swift would then manage the subscription and unsubscribe for me. Turns out, I was expecting too much.

Having put this code in, the onReceive still fires even after the window has been closed which implies that the view is still there even after the window closes.

This has to be something to do with how the parent NSWindow is hosting the SwiftUI view. I suspect I am going to have to get creative.

3      

Maybe this from the documentation in Swift.Org will help. Assigning your ViewModel instance to 'nil' should remove the class and trigger the deinit, and its action.

3      

How are you instantiating the window? You mention NSWindow. Are you using the AppKit NSHostingController instead of the SwiftUI WindowGroup? It might be useful to show your code.

If the user closes a first window and then opens a second window, would the user desire the new window to start with a default state, ignoring the state of the previously closed window?

Have you tested whether vm.deinit() is called when you open the second window per the preceding paragraph? The documentation for StateObject says it is retained until the view that instantiated it changes its identity. I speculate that the identity of the view does not change until you open the 2nd window.

3      

Thanks for the responses...

@Greenamberred I would set my view model to nil if I knew the window was closing. I've tried to code a .onDisappear modifier, but that never gets called. So my view never knows it has gone away.

@bobstern I've done nothing special to create the window. My main window is defined in a WindowGroup. I open subsequent windows using File->New, as provided in the default menu code. Each window opens in it's default state with it's own view model.

@main
struct TemplateAppApp: App {

    @State var appState = AppState()
    @AppStorage("displayMode") var displayMode: DisplayMode = .auto

    var body: some Scene {
        WindowGroup {
            MainView()
                .environmentObject(appState)
                .onAppear {
                    setDisplayMode()
                }
        }
        .windowResizability(.contentMinSize)
        .defaultSize(width: Constants.mainWindowWidth, height: Constants.mainWindowHeight)

        .onChange(of: displayMode, perform: { _ in
            setDisplayMode()
        })

        .commands {
            Menus(appState: appState)
        }

        Settings {
            SettingsView()
        }
    }

    fileprivate func setDisplayMode() {
        switch displayMode {
        case .light:
            NSApp.appearance = NSAppearance(named: .aqua)
        case .dark:
            NSApp.appearance = NSAppearance(named: .darkAqua)
        case .auto:
            NSApp.appearance = nil
        }
    }
}

The MainView then creates simple content, including the View Model:

import SwiftUI

struct MainView: View {

    @EnvironmentObject() var appState: AppState
    @StateObject var vm: MainViewModel = MainViewModel()
    @State var windowNumber: Int = 0

    var body: some View {
        NavigationSplitView(sidebar: {
            SidebarView(vm: vm)
                .frame(minWidth: Constants.mainWindowSidebarMinWidth)
        }, detail: {
            DetailView(vm: vm)
        })
        .frame(minWidth: Constants.mainWindowMinWidth,
               minHeight: Constants.mainWindowMinHeight)
        .navigationTitle(Constants.mainWindowTitle)

        .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { newValue in
            guard let win = newValue.object as? NSWindow else { return }
            if win.windowNumber == windowNumber {
                vm.reset()
            }
        }

        HostingWindowFinder { window in
          if let window = window {
              self.appState.addWindowAndModel(window: window,
                                              viewModel: vm)
              windowNumber = window.windowNumber
          }
        }.frame(height: 0)

    }
}

As a work round, I coded the .onReceive() modifier to handle willCloseNotification messages to check when i get a notification that my window is closing. Since I get that notification for every closing window, I have had to add code to get the windowNumber and check it against the window being closed.

HostingWindowFinder is a helper view used to get a reference to my NSWindow, which also lets me know my current windowNumber.

struct HostingWindowFinder: NSViewRepresentable {
    var callback: (NSWindow?) -> Void

    func makeNSView(context: Self.Context) -> NSView {
        let view = NSView()
        DispatchQueue.main.async { [weak view] in
            self.callback(view?.window)
        }
        return view
    }
    func updateNSView(_ nsView: NSView, context: Context) {}
}

It's a bit (very) hacky and I would prefer to understand why my view isn't being destroyed along with it's view model.

Now I'm looking at it, I'm wondering whether the global appState is hanging on to a reference to the window. It shouldn't be, but it gives me another direction to look.

3      

I may be closing in on the problem. In this component:

        HostingWindowFinder { window in
          if let window = window {
              self.appState.addWindowAndModel(window: window,
                                              viewModel: vm)
              windowNumber = window.windowNumber
          }
        }.frame(height: 0)

I call a function on the global appState to record my window and view model details. I'm using this for communication purposes, so needed a reference to the window. When I comment this code out, my deinit code is called.

So I guess something in the appState is hanging on to the view/view model. I have no idea how I'm going to fix this, but it seems it's entirely self inflicted!

Always irritating when you have fully working code that turns out to be doing more than you expected it to.

Thanks for the help. Steve

3      

Yep, my fault. The appState is called so I can set the window delegate in order to handle two messages (when the window closes and when it becomes the active window). By setting myself as the delegate I must be breaking the existing window functionality so the window isn't destroyed properly.

Always said a little knowledge was dangerous.

so, unless I can define multiple delegates, that's three weeks work I'll have to re-imagine and re-work.

3      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.