Вывод и закрытие Windows в приложении MacOS SwiftUI

У меня есть этот небольшой пример приложения, которое создает несколько окон моего приложения SwiftUI MacOS.
Является ли это возможным:

  • иметь список всех открытых окон в MainView?
  • закрыть одно окно из MainView?
  • отправить сообщение в одно окно из MainView?
      @main
struct MultiWindowApp: App {
  @State var gvm = GlobalViewModel()
  var body: some Scene {
    WindowGroup {
      MainView()
      .environmentObject(gvm)
    }
    WindowGroup("Secondary") {
      SecondaryView(bgColor: .blue)
      .environmentObject(gvm)
    }
    .handlesExternalEvents(matching: Set(arrayLiteral: "*"))
  }
}


struct MainView: View {
  @Environment(\.openURL) var openURL
  @EnvironmentObject var vm : GlobalViewModel

  var body: some View {
    VStack {
      Text("MainView")
      Button("Open Secondary") {
        if let url = URL(string: "OpenNewWindowApp://bla") {
          openURL(url)
        }
       //List of all open Windows
          // Button to close a single window
          // Button to set color of  a single window to red
      }
    }
    .padding()
  }
}

struct SecondaryView: View {
  var bgColor : Color

  @EnvironmentObject var vm : GlobalViewModel

  var body: some View {
    VStack{
      Spacer()
      Text("Viewer")
      Text("ViewModel: \(vm.name)")
      Button("Set VM"){
        vm.name = "Tom"
      }
      Spacer()
    }
    .background(bgColor)
    .frame(minWidth: 300, minHeight: 300, idealHeight: 400, maxHeight: .infinity, alignment: .center )
  }
}

class GlobalViewModel :ObservableObject {
  @Published var name = "Frank"
}

1 ответ

Решение

Возможно, есть более ориентированный на SwiftUI способ сделать это. Если еще нет, я определенно надеюсь, что Apple добавит более качественные средства управления окнами для Mac - сейчас все кажется чем-то вроде взлома.

Вот что я придумал:

      
@main
struct MultiWindowApp: App {
  @State var gvm = GlobalViewModel()
  var body: some Scene {
    WindowGroup {
      MainView()
      .environmentObject(gvm)
    }
    WindowGroup("Secondary") {
      SecondaryView(bgColor: .blue)
      .environmentObject(gvm)
    }
    .handlesExternalEvents(matching: Set(arrayLiteral: "*"))
  }
}


struct MainView: View {
  @Environment(\.openURL) var openURL
  @EnvironmentObject var vm : GlobalViewModel

  var body: some View {
    VStack {
      Text("MainView")
        List {
            ForEach(Array(vm.windows), id: \.windowNumber) { window in
                HStack {
                    Text("Window: \(window.windowNumber)")
                    Button("Red") {
                        vm.setColor(.red, forWindowNumber: window.windowNumber)
                    }
                    Button("Close") {
                        window.close()
                    }
                }
                
            }
        }
      Button("Open Secondary") {
        if let url = URL(string: "OpenNewWindowApp://bla") {
          openURL(url)
        }
      }
    }
    .padding()
    .frame(maxWidth: .infinity, maxHeight: .infinity)
  }
}

struct SecondaryView: View {
  var bgColor : Color
  @EnvironmentObject var vm : GlobalViewModel

    @State private var windowNumber = -1
    
  var body: some View {
    VStack{
        HostingWindowFinder { window in
            if let window = window {
                vm.addWindow(window: window)
                self.windowNumber = window.windowNumber
            }
        }
      Spacer()
      Text("Viewer")
      Text("ViewModel: \(vm.name)")
      Button("Set VM"){
        vm.name = "Tom"
      }
      Spacer()
    }
    .background(vm.backgroundColors[windowNumber] ?? bgColor)
    .frame(minWidth: 300, minHeight: 300, idealHeight: 400, maxHeight: .infinity, alignment: .center )
  }
}

class GlobalViewModel : NSObject, ObservableObject {
    @Published var name = "Frank"
    @Published var windows = Set<NSWindow>()
    @Published var backgroundColors : [Int:Color] = [:]
    
    func addWindow(window: NSWindow) {
        window.delegate = self
        windows.insert(window)
    }
    
    func setColor(_ color: Color, forWindowNumber windowNumber: Int) {
        backgroundColors[windowNumber] = color
    }
}

extension GlobalViewModel : NSWindowDelegate {
    func windowWillClose(_ notification: Notification) {
        if let window = notification.object as? NSWindow {
            windows = windows.filter { $0.windowNumber != window.windowNumber }
        }
    }
}

struct HostingWindowFinder: NSViewRepresentable {
    var callback: (NSWindow?) -> ()

    func makeNSView(context: Self.Context) -> NSView {
        let view = NSView()
        DispatchQueue.main.async { [weak view] in
            self.callback(view?.window)
        }
        return view
    }
    func updateNSView(_ nsView: NSView, context: Context) {}
}

Я использую трюк из https://lostmoa.com/blog/ReadingTheCurrentWindowInANewSwiftUILifecycleApp/,

чтобы получить ссылку на NSWindow. Это сохраняется в модели представления в наборе. Позже, чтобы получить доступ к таким вещам, как закрытие окон и т.д., я ссылаюсь на окна с windowNumber.

Когда появляется окно, оно добавляется в список окон модели представления. Затем, когда модель представления получит windowWillClose вызов в качестве делегата, он удаляет его из списка.

Настройка цвета фона осуществляется через backgroundColorsсвойство на модели представления. Если нет одного набора, он использует переданное свойство цвета фона. Есть масса разных способов спроектировать этот бит.

Другие вопросы по тегам