Search code examples
swiftxcodemacosswiftui

What is the difference between ObservableObject and @State to store a NSWindow object


I am dynamically/programatically creating windows like this:

func createWelcomeWindow() {
    let w = WelcomeScreenView().frame(width: 400, height: 600)
    let hostingController = NSHostingController(rootView: w)
    appState.welcomeWindow = NSWindow(contentViewController: hostingController)
    appState.welcomeWindow?.title = "Welcome to Last Warning"
    appState.welcomeWindow?.collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary]
    appState.welcomeWindow?.makeKeyAndOrderFront(nil)
}

The appState is being created so:

class AppState: ObservableObject {
    @Published var warningWindow: NSWindow?
    @Published var welcomeWindow: NSWindow?
}

@main
struct LastWarningApp: App {
    @ObservedObject var appState = AppState()

I am confused about state management in swift.

I tried, mainly out of curiousity, changing for

@main
    struct LastWarningApp: App {
        @State var welcomeWindow: NSWindow?

And then doing:

welcomeWindow = NSWindow(contentViewController: hostingController)
welcomeWindow?.title = "Welcome to Last Warning"
welcomeWindow?.collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary]
welcomeWindow?.makeKeyAndOrderFront(nil)

But this doesn't create the window, I am wondering why this is, as far as I've understood asking chatGPT, @State seems to be for managing the state of the views and is not suitable for this, I've gone through as much github repos as possible trying to see best practice and I couldn't find clear ways to go about this.

One thing I've noticed, but I am not sure about it, is taht welcome window seems to not be set when I debug it.

So is this a reasonable approach? And why doesn't @State work in this particular case?

It also confuses me that I can do:

@State var appState = AppState()

And behaviour stays the same.


Solution

  • @State works as a source of truth and redraws the body when the “value” changes.

    There isn’t anything wrong with

    @State var appState = AppState()
    

    You just have to be aware that changing a property of a reference type does not trigger a redraw because the value isn’t changing. <<<<< Very important

    ObservableObject need a series of things to work.

    @Published emitting when the value changes > ObservableObject synthesizing the emission > an “Object” property wrapper to invalidate the View.

    Now the “gap”, reference types that aren’t ObservableObject or Obervable, These can be stored with @State but won’t redraw the View because there is no mechanism.

    You may need storage within SwiftUI for these because View is a value type and can be recreated at any time by SwiftUI. You will end up with multiple instances if you don’t use it.

    There is a difference with the “Object” property wrappers. StateObject is for initializing and ObservedObject is for passing around.

      @ObservedObject var appState = AppState()
    

    Is incorrect…..

    You should use

       @StateObject var appState = AppState()
    

    The main difference behind this is having access to the secret storage SwiftUI has.

    In iOS 13 “the right” way to initialize an ObservableObject was with State but we couldn’t observe it until the next view where we could use ObservedObject. StateObject was introduced in iOS 14.

    Now this is the basics of the SwiftUI property wrappers but I think your biggest issue is that you are mixing SwiftUI and AppKit SwiftUI has a very different way of managing windows.

    https://developer.apple.com/videos/play/wwdc2022/10061/

    https://developer.apple.com/documentation/swiftui/bringing_multiple_windows_to_your_swiftui_app

    After you design the windows in your App.

    @main
    struct BookClub: App {
        @StateObject private var store = ReadingListStore()
    
        var body: some Scene {
            WindowGroup {
                ReadingListViewer(store: store)
            }
            Window("Activity", id: "activity") {
                ReadingActivity(store: store)
            }
        }
    }
    

    You present them something like

    struct OpenWindowButton: View {
        @Environment(\.openWindow) private var openWindow
    
        var body: some View {
            Button("Open Activity Window") {
                openWindow(id: "activity")
            }
        }
    }