如何在SwiftUI中打开另一个窗口,macOS?

26

我想在macOS上的SwiftUI应用程序中显示一个带有不同内容的第二个窗口。我找不到任何关于它的文档资料。下面的尝试不起作用。有人知道如何做吗?

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.")
    }
}
7个回答

18

这是一个用于创建一个带标题并打开的 NSViewController 窗口的扩展。

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
    }
}

使用方法:

Button("Open 2nd Window") {
    SecondContent().openInWindow(title: "Win View", sender: self)
}

关闭窗口

NSApplication.shared.keyWindow?.close()

1
使用以下代码来编程关闭窗口:NSApplication.shared.keyWindow?.close() - Marcos Tanaka
我在 Xcode 14 中尝试了这个,没有出现任何错误,但是当按钮被点击时也没有任何反应。 - Subcreation
当您到达此行 win.makeKeyAndOrderFront(sender) 时,它应该像任何其他 NSWindow 一样工作,因为它 NSWindow。您可以打开任何窗口吗?也许与 SwiftUI 无关?您可以尝试从窗口访问 contantViewController 并检查它。 - bauerMusic
这会打开一个大小为零的窗口。所以你看不到它,需要调整大小。 - Tbi

16

在Xcode 13 beta和SwiftUI 3.0上进行过测试

在遇到这种情况后,我整合了一些遍布互联网的答案,并且以下方法适用于我:

在@main (MyAppApp)文件中添加所需数量的WindowGroup("窗口名称")

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"))

}

每个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 ().

现在,在MyAppApp文件的末尾(在struct MyAppApp: App之外),添加以下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)
        }
    }
}

请将以下内容添加到您的Info.plist

Info.plist

将myapp替换为您的应用程序名称。

使用方法:

Button(action: {
            OpenWindows.SecondView.open()
       }){
            Text("Open Second Window")           
         }

有没有办法在“设置”场景中完成这个操作?“handlesExternalEvents”显示错误,说它仅适用于WindowGroups。 - baguIO
1
你为什么要使用 Set(arrayLiteral:)?这很奇怪;那个方法只应该由编译器调用。 - Peter Schorn
1
很不幸地,这在macOS 13 Beta 1上无法工作。 - ixany
1
我已经在Xcode 13.4.1中尝试过了,但它每次只是打开第一个窗口的主ContentView。我有什么遗漏吗? - iphaaw

3

2022年6月更新:现在已经添加了窗口API。我将保留旧答案,因为它可能仍然对那些需要外部链接的人有用。

您要查找的方法是WindowGroupViewhandlesExternalEvents。您还需要首先创建一个URL方案来识别您的书籍,并将其添加到您的Info.plist中。当调用@EnvironmentopenURL时,如果具有与书籍URL匹配的handlesExternalEvents的视图已经在窗口中,则会重新激活该窗口。否则,它将使用应用于WindowGrouphandlesExternalEvents来打开新窗口。

您可以在我的博客这里上看到示例。


2
太棒了,博客中的代码对我有用。我一直在寻找这样的解决方案。谢谢。 - MacUserT
@malhal 我知道你想要通过增长黑客来推广你的博客。但现在链接已经失效,答案也不见了... - Wukerplank
已经修复了,但是你应该使用新的窗口API。 - malhal

2
在macOS 13中,你现在可以通过编程方式进行窗口的注册打开
@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)
        }
    }
}

(来自).

我很开心在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()
    }
}

这太糟糕了,特别是使用NSHostingController时会破坏一些东西,比如toolbar修饰符。

如果您正在寻找一些旧的解决方案,它可能仍然有所帮助;)


2

这将会打开一个新窗口:

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()
    }
}

2
这是在 SwiftUI 2.0 之前的旧方式。 - MacUserT
4
如果这是“旧”的方式,你可以加上“新”的方式吗? - Frank Nichols
3
如果“新”的方法是使用自定义URL方案来创建新窗口,那么这种方法很糟糕。这意味着你需要将每个窗口“硬编码”到info.plist文件中。总之感觉都不对劲。我宁愿使用NSHostingView而不是将窗口添加到info.plist中。 - bauerMusic
不需要在 info.plist 中硬编码每个窗口,只需使用 URL Scheme 即可。 - Serge Ivamov
1
当我关闭一个像那样创建的窗口时,应用程序会在@main中崩溃,我们知道原因吗? - Mojo66
在Xcode 14中,这并没有做任何事情。 - Subcreation

0

我写了一个快速的要点 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)
    }
}

0
关于官方的WWDC指南,解决方案是使用Window类和@Environment(\.openWindow) private var openWindow
Scheme不是一个合适的解决方案。
例如,
var body: some Scene {
        Window("FirstView", id: "FirstView") {
            FirstView(viewModel: viewModel)
        }
        Window("SecondView", id: "SecondView") {
            SecondView(viewModel: viewModel)
        }
    }

FirstView打开SecondView的方法是:
struct FirstView: View {
@Environment(\.openWindow) private var openWindow

var body: some View {
// code code
// tap handler
openWindow(id: "SecondView")
}
}

就这样。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接