Как открыть другое окно в SwiftUI, macOS?

#macos #swiftui

#macos #swiftui

Вопрос:

Я хотел бы показать второе окно с другим содержимым в приложении SwiftUI на macOS. Я не могу найти никакой документации по этому вопросу. Попытка ниже не работает. Кто-нибудь знает, как это сделать?

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

Ответ №1:

Протестировано на Xcode 13 beta, SwiftUI 3.0

После того, как я оказался в этой ситуации, я получил несколько ответов, которые были по всему Интернету, и это работает для меня:

В файле @main (MyAppApp) добавьте WindowGroup("Window Name") необходимое вам количество:

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

Комментарии:

1. Есть ли способ сделать это со Settings сценой? handlesExternalEvents показывает ошибку, в которой говорится, что она доступна только для оконных групп

2. Почему вы используете Set(arrayLiteral:) ? Это очень странно; этот метод должен вызываться только компилятором.

3. К сожалению, это не работает на macOS 13 Beta 1.

4. Я пробовал это в Xcode 13.4.1, и все, что он делает, это каждый раз открывает основной contentView из первого окна. Я что-то упустил?

Ответ №2:

Вот расширение для создания окна 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()

2. Я пробовал это в Xcode 14, и я не получаю никаких ошибок, но при нажатии кнопки тоже ничего не происходит

3.Когда вы дойдете до этой строки win.makeKeyAndOrderFront(sender) , она должна работать как любая другая NSWIndow , поскольку это NSWindow .. Вы можете открыть любое окно? Может быть, не связано с SwiftUI? Вы можете попытаться получить доступ к contantViewController из окна и проверить его.

Ответ №3:

Обновление июнь 2022: теперь добавлены оконные API. Я оставлю старый ответ ниже, поскольку он все еще может быть полезен для тех, кому нужна внешняя ссылка.

Метод, который вы ищете, — это WindowGroup and View ‘s handlesExternalEvents . Вам также необходимо сначала создать схему URL для идентификации вашей книги, добавить ее в свой Info.plist. При вызове @Environment ‘s openURL , если представление handlesExternalEvents , соответствующее URL-адресу книги, уже находится в окне, оно повторно активирует это окно. В противном случае он будет использовать handlesExternalEvents applied to WindowGroup для открытия нового окна.

Вы можете увидеть пример в моем блоге здесь.

Комментарии:

1. Ура, код в блоге сработал для меня. Я искал такое решение. Спасибо.

Ответ №4:

Откроется новое окно:

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

Комментарии:

1. Это старый способ до SwiftUI 2.0.

2. Если это «старый» способ, не могли бы вы добавить «новый» способ?

3. Если «новый» способ использует пользовательскую схему URL, это ужасный способ создания нового окна. Это означает, что вам нужно «жестко закодировать» каждое окно в info.plist файл. Это просто кажется неправильным во всех отношениях. Я скорее просто использую NSHostingView , чем добавляю окна в info.plist .

4. Нет необходимости в «жестком коде» для каждого окна в info.plist . URL Scheme достаточно.

5. Когда я закрываю окно, которое было создано таким образом, приложение вылетает в @main, мы знаем, почему?

Ответ №5:

В macOS 13 теперь вы можете зарегистрировать и открыть windows программно:

 @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 модификатор.

Если вы ищете какое-то обратное решение, оно все равно может помочь 😉