10

I would like to have a button to open a new window and load a view (for the app's Preferences) in SwiftUI for MacOS but am unsure of the correct way to do it.

I tried creating a function and calling it from the button action which works but on closing the new window the app crashes with:

Thread 1: EXC_BAD_ACCESS (code=1, address=0x20)

This is my function:

func openPreferencesWindow() {
    var preferencesWindow: NSWindow!
    let preferencesView = PreferencesView()
    // Create the preferences window and set content
    preferencesWindow = NSWindow(
        contentRect: NSRect(x: 20, y: 20, width: 480, height: 300),
        styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
        backing: .buffered,
        defer: false)
    preferencesWindow.center()
    preferencesWindow.setFrameAutosaveName("Preferences")
    preferencesWindow.contentView = NSHostingView(rootView: preferencesView)
    preferencesWindow.makeKeyAndOrderFront(nil)
}

And this is my button calling it:

Button(action: {
    openPreferencesWindow()
}) {
    Text("Preferences").font(.largeTitle).foregroundColor(.primary)
}

I feel like the window should be constructed in AppDelegate but I'm not sure how I would then call it.

mousebat
  • 474
  • 7
  • 25

1 Answers1

11

You need to keep reference to created preferences window (like to the main window).

Here is possible solution (tested with Xcode 11.4 / macOS 10.15.5)

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {

    var window: NSWindow!
    var preferencesWindow: NSWindow!    // << here

    @objc func openPreferencesWindow() {
        if nil == preferencesWindow {      // create once !!
            let preferencesView = PreferencesView()
            // Create the preferences window and set content
            preferencesWindow = NSWindow(
                contentRect: NSRect(x: 20, y: 20, width: 480, height: 300),
                styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
                backing: .buffered,
                defer: false)
            preferencesWindow.center()
            preferencesWindow.setFrameAutosaveName("Preferences")
            preferencesWindow.isReleasedWhenClosed = false
            preferencesWindow.contentView = NSHostingView(rootView: preferencesView)
        }
        preferencesWindow.makeKeyAndOrderFront(nil)
    }

    // ... other code

and now button would look like

Button(action: {
    NSApp.sendAction(#selector(AppDelegate.openPreferencesWindow), to: nil, from:nil)
}) {
    Text("Preferences").font(.largeTitle).foregroundColor(.primary)
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Works great thank you! Can you explain to me (I'm not very experienced in objective c) the mechanism of this so I understand? 1. Why must the @objc property be applied to the function? 2. Why do we have to call the function with NSApp instead of calling it directly. 3. Why does the NSWindow instance always have to have a reference in AppDelegate? – mousebat Jul 08 '20 at 08:43
  • Yep, I would also like to know. It worked for me without all the 3 points. – Kai Zheng Sep 05 '20 at 14:34
  • Excellent solution @Asperi, as usual! Thank you so much. – ixany Dec 15 '20 at 10:08
  • How can I add toolbar Item or a navigationTitle to that view? The default way with .toolbar / .navigationTitle is somehow not working. – DoTryCatch Jan 23 '21 at 15:25
  • @HackMac `window.title = "Window title"`. When using `NSHostingView`, `navigationTitle` does not work, you need to set the title right on the window. – bauerMusic May 09 '21 at 05:23
  • @bauerMusic, reverted as not related to topic starter question. Any not related question should be posted/answered as separated question. – Asperi May 09 '21 at 07:12
  • @Asperi Sure, got it. It's here as a comment, that's good enough. – bauerMusic May 09 '21 at 15:22