0

I have an app that has no startup window. So in my @main there is no windowGroup.

var body: some Scene {
#if os(macOS)
  Settings {
           
  }
#endif
}

But in app somewhere, It is needed to create a window. (Playground runnable)

import SwiftUI
import AppKit
import PlaygroundSupport
import Foundation

class AppWindow: NSWindow, ObservableObject {
    
}

struct ContentView: View {
    @StateObject private var viewModel: ViewModel = .init()
    var body: some View {
        Text(viewModel.hello)
    }
}

class ViewModel: ObservableObject {
    @Published var hello = "Hello"
    
    init() {
        print("init")
    }
    
    deinit {
        print("deinit")
    }
}

struct PlaygroundView: View {
    @State private var window: AppWindow? = nil

    
    var body: some View {
        Button {
            createWindow()
        } label: {
            Text("open window")
        }
    }
    
    func createWindow() {
        if let curWindow = window {
            curWindow.close()
            window = nil
        }
        let window = AppWindow(
            contentRect: NSRect(x: 0, y: 0, width: 100, height: 100),
            styleMask: [.titled, .closable, .miniaturizable, .resizable],
            backing: .buffered,
            defer: false)
        window.center()
        window.contentViewController = NSHostingController(rootView:
                                                            ContentView()
            .frame(width: 100, height: 100, alignment: .center)
        )
        window.makeKeyAndOrderFront(nil)
        window.isReleasedWhenClosed = true
        self.window = window
    }
}

PlaygroundPage.current.setLiveView(PlaygroundView())

everything is ok. The ViewModel will print the "init" or "deinit" when the window is being opened or closed. Until in my ContentView, I want to do something like setFrame(.. to the window.

struct ContentView: View {
    @EnvironmentObject var window: AppWindow // <--- added
    @StateObject private var viewModel: ViewModel = .init()
    var body: some View {
        Text(viewModel.hello)
    }
}
...
window.contentViewController = NSHostingController(rootView:
                                                            ContentView()
            .frame(width: 100, height: 100, alignment: .center)
            .environmentObject(window)  // <--- added
        )
...

The deinit will not be called when the window is closed.

Chocoford
  • 183
  • 8

1 Answers1

0

If you are talking about model data, the object that holds the model data is not usually supposed to be deinit during lifetime of the application. It usually is a singleton to safeguard against this and so we can still use data for previewing (@StateObject are not init for previews), e.g.

struct ContentView: View {
    @EnvironmentObject var store: Store

    var body: some View {
        ForEach(store.messages) { message in
            Text(message.text)
        }
    }
}

struct Message: Identifiable {
    let id = UUID()
    var text: String
}

class Store: ObservableObject {
    @Published var messages: [Message] = [Message(text: "Hello")]

    static void shared = Store()
    static void preview = Store(preview: true) // for previews
    
    init(preview: Bool = false) {
        if preview {
            // configure using sample data
        }
        print("init")
    }
    
    // methods for persisting/syncing model data
}

Use it like

ContentView().environmentObject(Store.shared)

And in previews like

ContentView().environmentObject(Store.preview)

If you are talking about view data, then it is simply:

struct ContentView: View {
    @State var messages = [Message(text: "Hello")]

Pass it to a subview as a let if you need read access or mark it as @Binding if you need write access. SwiftUI will track the dependency and body will be called when the value of the let or the state/binding changes.

We don't use objects for view data because it would break SwiftUI's features like diffing and dependency tracking of state that rely on value semantics to work correctly and prevent consistency bugs. The View struct stores the data and the @State/@Binding property wrappers makes it behave like an object, that's your view model right there, if you made an actual object for this then it kinda defeats the purpose of SwiftUI and you would lose out on great features like automatic localization of Text.

malhal
  • 26,330
  • 7
  • 115
  • 133