How to open a window in SwiftUI?

Its great to have WindowGroup and Scenes - but how do we open one of these windows on macOS programatically or from a menu item?

How do we decide what windows open on launch?

Post not yet marked as solved Up vote post of lupinglade Down vote post of lupinglade
15k views

Replies

I'm new to macOS development and I also have this question...
Are you able to get more than one window type to show up? From what I'm seeing this doesn't appear to support more than one Scene (aside from Settings). But there is "id" for the WindowGroup so that suggests it should be possible.
You can also make a group of Scenes, but only the first one is displayed. I’d really like on iOS to transition from one scene to another (specifically a WindowGroup to a DocumentGroup) and on macOS to have the second scene to open in a new window. Why allow more than one scene per app if you can’t show one from a button click just like with Settings()?
To my knowledge AppDelegate is the only way for now, place it in you App file as a var like so

Code Block swift
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    var body: some Scene {
        Settings {
        }
}


Then have your window class

Code Block swift
class AppWindow: NSWindow {
    init() {
        super.init(contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false)
        makeKeyAndOrderFront(nil)
        isReleasedWhenClosed = false
        styleMask.insert(NSWindow.StyleMask.fullSizeContentView)
        title = "title placeholder"
        contentView = NSHostingView(rootView: ContentView())
    }
}



I'm guessing (hoping) this is coming in the next seed.
Really hoping this makes it into release!
Any news on this? Code like the following doesn't seem to work for me. (Xcode 12.2 beta, Big Sur beta).

Code Block swift
var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(appState)
        }
        WindowGroup("File List") {
            if appState.showFileList {
                FileListView().environmentObject(appState)
            }
        }
    }



There is a hacky way to do this. You can create a View that acts as your app's root view. The body of this view depends on the value of a specific variable.

For example:
Code Block
@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            RootView()
        }
    }
}
var useWindow1: Bool = true
struct RootView: View {
    var body: some View {
        Group {
            if useWindow1 {
                Window1()
            } else {
                Window2()
            }
        }
    }
}


You can then use the NSWindowController on the MainWindow to open a new window.

Code Block
 NSApp.mainWindow?.windowController?.newWindowForTab(nil)

Though this does require that at least one window is visible. There could be another way to trigger the "New Window" command, but I haven't found one that works.

One upshot of using this instead of creating a new NSWindow with an NSHostingController is that toolbars won't display their items if you just put them in a Hosting Controller, since the NSWindow thinks it should manage the toolbar.

Also, you may have to override some of macOS' window restoration behavior, since if you have two windows of two separate views when you quit the app, it may relaunch with two of the "default" view.
Yes to open a new window in a SwiftUI macOS app use @Environment openURL, WindowGroup's handlesExternalEvents and Project->Info->URL Types.

Below is a copy of a tutorial I posted on my blog (I wasn't allowed to include a link to it).

Here is how to open a new window in SwiftUI on macOS.
In your ContentView create a button and open a URL for your app and another View e.g. Viewer to be shown in the window we will open:
Code Block
struct ContentView: View {
@Environment(\.openURL) var openURL
var body: some View {
VStack {
Button("Open Viewer") {
if let url = URL(string: "myappname://viewer") {
openURL(url)
}
}
Text("Hello, world!")
}
.padding()
}
}
struct Viewer: View {
var body: some View {
Text("Viewer")
}
}

In your App add another WindowGroup for your viewer and set it to enable handling of external launch events (an internal event in our case).
Code Block
@main
struct GroupDefaultsTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
WindowGroup("Viewer") { // other scene
Viewer()
}
.handlesExternalEvents(matching: Set(arrayLiteral: "*"))
}
}

Now in Project->Info->URL Types type in myappname in the URL Schemes field (and the identifier field too) to register our app with the system.
Now run your app and click the button and it should open a new window!
Post not yet marked as solved Up vote reply of malc Down vote reply of malc
  • This is the best solution for macOS 13 or earlier if we want to open a new window in a SwiftUI macOS app. Thanks for your sharing and I get a lot from your professional answer. For macOS 13 and later, we can use openWindow method to solve this problem more elegantly.

Add a Comment

Tested on Xcode 13 beta, SwiftUI 3.0

After having being in this situation, I Frankensteined some answers that where all over the internet and this works for me:

On the @main (MyAppApp) file add the amount of WindowGroup("Window Name") you need:

import SwiftUI

@main
struct MyAppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        
        WindowGroup("Second Window") {
            SecondWindow()
        }.handlesExternalEvents(matching: Set(arrayLiteral: "SecondWindow"))
        
        WindowGroup("Third Window") {
            ThirdWindow()
        }.handlesExternalEvents(matching: Set(arrayLiteral: "ThirdWindow"))

}

What to place in every WindowGroup?:

WindowGroup("SecondWindow") /*Any name you want to be displayed at the top of the window.*/ {
            SecondWindow() //View you want to display.
}.handlesExternalEvents(matching: Set(arrayLiteral: "SecondWindow")) //Name of the view without ().

Now, at the end of the MyAppApp file (outside of the struct MyAppApp: App) add the following enum :

enum OpenWindows: String, CaseIterable {
    case SecondView = "SecondView"
    case ThirdView   = "ThirdView"
    //As many views as you need.

    func open(){
        if let url = URL(string: "myapp://\(self.rawValue)") { //replace myapp with your app's name
            NSWorkspace.shared.open(url)
        }
    }
}

Add the following to your Info.plist

Replace myapp with your app's name.

Usage:

Button(action: {
            OpenWindows.SecondView.open()
       }){
            Text("Open Second Window")           
         }
  • I had to enter the URL Schemes under "URL Types" in my project target's settings. Then it worked.

    But the window that got opened is very tiny. How to set a frame or size for the window?

  • Thank you so much. This worked like a charm

  • NSWorkspace.shared.open is not aailable in Mac Catalyst. Any solution?

I have found this also to work. Although I think SwiftUI needs a much better way of supporting multiple macOS Windows.

In your AppStruct, although any persistent struct or class can hold a window.

Limitations: No idea how to let the system determine the window position or whether it will autosave window information.

struct Radio_2App: App {

    let someWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 580, height: 300), styleMask: [.titled, .closable],  backing: .buffered, defer: false)

    var body: some Scene {

        WindowGroup() {
              SomeContentView()
        }

    func openWindow() {
        someWindow.contentView = NSHostingView(rootView: SomeOtherView())
        self.someWindow.makeKeyAndOrderFront(nil)
    }

}

view extension:

extension View {
  private func newWindowInternal(title: String, geometry: NSRect, style: NSWindow.StyleMask, delegate: NSWindowDelegate) -> NSWindow {
    let window = NSWindow(
      contentRect: geometry,
      styleMask: style,
      backing: .buffered,
      defer: false)
    window.center()
    window.isReleasedWhenClosed = false
    window.title = title
    window.makeKeyAndOrderFront(nil)
    window.delegate = delegate
    return window
  }
   
  func openNewWindow(title: String, delegate: NSWindowDelegate, geometry: NSRect = NSRect(x: 20, y: 20, width: 640, height: 480), style:NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable]) {
    self.newWindowInternal(title: title, geometry: geometry, style: style, delegate: delegate).contentView = NSHostingView(rootView: self)
  }
}

call with:

Text("This is a swiftui text").openNewWindow(...)

Make sure to keep a strong reference to the window delegate, the window will keep only a weak ref.

  • This worked perfectly for me! Thank you so much!

Add a Comment