Reputation: 16399
I'd like to show a second window with different content in a SwiftUI app on macOS. I can't find any documentation on this. The attempt below doesn't work. Does anyone know how to do it?
class AppState: ObservableObject {
@Published var showSecondWindow: Bool = false
}
@main
struct MultipleWindowsApp: App {
@StateObject var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(appState)
}
WindowGroup {
if appState.showSecondWindow {
SecondContent()
}
}
}
}
struct ContentView: View {
@EnvironmentObject var appState: AppState
var body: some View {
VStack {
Text("Hello, world!")
Button("Open 2nd Window") {
appState.showSecondWindow = true
}
}.padding()
}
}
struct SecondContent: View {
var body: some View {
Text("Hello, from window #2.")
}
}
Upvotes: 28
Views: 12423
Reputation: 6156
Here's an extension for creating a window for NSViewController
, assigning a title and opening it.
extension View {
@discardableResult
func openInWindow(title: String, sender: Any?) -> NSWindow {
let controller = NSHostingController(rootView: self)
let win = NSWindow(contentViewController: controller)
win.contentViewController = controller
win.title = title
win.makeKeyAndOrderFront(sender)
return win
}
}
Usage:
Button("Open 2nd Window") {
SecondContent().openInWindow(title: "Win View", sender: self)
}
To close the window
NSApplication.shared.keyWindow?.close()
Available from macOS 13, there's the @Environment(\.openWindow) var openWindow
. Major downside is that if you need to pass a @StateObject
or similar view model, it needs to be Codable
.
If transforming your view model to conform to Codable
is not practical, the above solution allows you to simply pass anything to a view's initializer.
Upvotes: 17
Reputation: 580
For an updated answer, check:
Obsolete:
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")
}
Upvotes: 17
Reputation: 27211
Regarding official WWDC guide the solution is using Window
class and
@Environment(\.openWindow) private var openWindow
Scheme isn't a proper solution
For example,
var body: some Scene {
Window("FirstView", id: "FirstView") {
FirstView(viewModel: viewModel)
}
Window("SecondView", id: "SecondView") {
SecondView(viewModel: viewModel)
}
}
To open SecondView
from FirstView
:
struct FirstView: View {
@Environment(\.openWindow) private var openWindow
var body: some View {
// code code
// tap handler
openWindow(id: "SecondView")
}
}
That's it.
Upvotes: 7
Reputation: 375
I wrote a quick gist View+NSWindow.swift
struct ContentView: View {
var body: some View {
Button(action: {
ChildView().openInNewWindow { window in
window.title = "Window title"
}
}) {
Image(systemName: "paperplane")
}
.padding()
}
}
struct ChildView: View {
var body: some View {
VStack {
Spacer()
HStack {
Spacer()
Text("Hello World")
.padding()
Spacer()
}
Spacer()
}
.frame(minWidth: 640, minHeight: 480)
}
}
Upvotes: 1
Reputation: 30549
Update June 2022: windowing APIs have now been added. I'll leave old answer below as it might still be useful for those that need external linking.
The method you are looking for is WindowGroup
and View
's handlesExternalEvents
. You also need to first create a URL scheme to identify your book, add it to your Info.plist. When calling @Environment
's openURL
, if a View with handlesExternalEvents
that matches the book's URL is already in a window then it'll re-activate that window. Otherwise it will use the handlesExternalEvents
applied to the WindowGroup
to open a new window.
You can see a sample on my blog here.
Upvotes: 3
Reputation: 4930
In macOS 13, you can now register and open windows programmatically:
@main
struct OpenWindowApp: App {
@StateObject private var dataStore = DataStore()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(dataStore)
}
WindowGroup("Note", for: Note.ID.self) { $noteId in
NoteView(noteId: noteId)
.environmentObject(dataStore)
}
}
}
(from source).
I had some fun recreating it in macOS 11:
import SwiftUI
private class WindowStorage {
static let main = WindowStorage()
private var blocks: [ObjectIdentifier: (AnyHashable) -> AnyView] = [:]
func viewContent<E: Hashable>(for element: E) -> AnyView? {
let type = ObjectIdentifier(E.self)
guard let view = blocks[type]?(element) else { return nil }
return view
}
func registerContent<E: Hashable, V: View>(block: @escaping (E) -> V) {
let type = ObjectIdentifier(E.self)
return blocks[type] = { anyHash in
guard let element = anyHash as? E else { return AnyView(EmptyView()) }
return AnyView(block(element))
}
}
}
extension View {
func openWindow<E: Hashable>(_ element: E) {
guard let view = WindowStorage.main.viewContent(for: element) else { return }
let root = NSHostingController(rootView: view)
let window = NSWindow(contentViewController: root)
window.toolbar = NSToolbar()
window.makeKeyAndOrderFront(self)
NSApplication.shared.mainWindow?.windowController?.showWindow(window)
}
}
extension WindowGroup {
init<E: Hashable, C: View>(for element: E.Type,
content: @escaping (E) -> C) where Content == WindowWrappingView {
self.init {
WindowWrappingView()
}
WindowStorage.main.registerContent(block: content)
}
}
struct WindowWrappingView: View {
var body: some View {
EmptyView()
}
}
It is terrible. Specially the usage of the NSHostingController
breaks things, like the toolbar
modifier.
If you are looking for some backward solution, it can still help ;)
Upvotes: 2
Reputation: 7204
This will open a new window:
import SwiftUI
struct ContentView: View
{
var body: some View
{
Button(action: {openMyWindow()},
label: {Image(systemName: "paperplane")})
.padding()
}
}
func openMyWindow()
{
var windowRef:NSWindow
windowRef = NSWindow(
contentRect: NSRect(x: 100, y: 100, width: 100, height: 600),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
windowRef.contentView = NSHostingView(rootView: WindowView())
windowRef.makeKeyAndOrderFront(nil)
}
struct WindowView: View
{
var body: some View
{
Text("Hello World")
.padding()
}
}
Upvotes: 2