swift - 如何在 macOS 的 SwiftUI 中使用菜单命令实现多窗口?

标签 swift macos swiftui

情况
实现一个多窗口应用程序,其中每个窗口都有自己的状态。
示例
这是一个展示问题的示例( on github ):

import SwiftUI

@main
struct multi_window_menuApp: App {

  var body: some Scene {
    WindowGroup {
      ContentView()
    }.commands {
      MenuCommands()
    }
  }
}

struct ContentView: View {
  @StateObject var viewModel: ViewModel  = ViewModel()
  
  var body: some View {
    TextField("", text: $viewModel.inputText)
      .disabled(true)
      .padding()
  }
}

public class ViewModel: ObservableObject {
  
  @Published var inputText: String = "" {
    didSet {
      print("content was updated...")
    }
  }
}
问题
我们应该如何以编程方式找出当前选择的 View 是什么,以便我们可以在菜单命令即将完成时更新状态并更新 View 模型中的状态?
import Foundation
import SwiftUI
import Combine

struct MenuCommands: Commands {
  
  var body: some Commands {
    CommandGroup(after: CommandGroupPlacement.newItem, addition: {
      Divider()
      Button(action: {
        let dialog = NSOpenPanel();
        
        dialog.title = "Choose a file";
        dialog.showsResizeIndicator = true;
        dialog.showsHiddenFiles = false;
        dialog.allowsMultipleSelection = false;
        dialog.canChooseDirectories = false;
        
        if (dialog.runModal() ==  NSApplication.ModalResponse.OK) {
          let result = dialog.url
          if (result != nil) {
            let path: String = result!.path
            do {
              let string = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8)
              print(string)
              // how to get access to the currently active view model to update the inputText variable?
              // viewModel.inputText = string
            }
            catch {
              print("Error \(error)")
            }
          }
        } else {
          return
        }
      }, label: {
        Text("Open File")
      })
      .keyboardShortcut("O", modifiers: .command)
    })
  }
}
可能有助于解决此问题的链接:
  • http://www.gfrigerio.com/build-a-macos-app-with-swiftui/
  • https://troz.net/post/2021/swiftui_mac_menus/
  • https://onmyway133.com/posts/how-to-manage-windowgroup-in-swiftui-for-macos/
  • 最佳答案

    有用的链接:

  • How to access NSWindow from @main App using only SwiftUI?
  • How to access own window within SwiftUI view?
  • https://lostmoa.com/blog/ReadingTheCurrentWindowInANewSwiftUILifecycleApp/

  • (这是我能够想出的,如果有人有更好的想法/方法,请分享)
    这个想法是创建一个共享的“全局” View 模型来跟踪打开的窗口和 View 模型。每个NSWindow有一个唯一的属性 windowNumber .当一个窗口变为事件(键)时,它会通过 windowNumber 查找 View 模型。并将其设置为 activeViewModel .
    import SwiftUI
    
    class GlobalViewModel : NSObject, ObservableObject {
      
      // all currently opened windows
      @Published var windows = Set<NSWindow>()
      
      // all view models that belong to currently opened windows
      @Published var viewModels : [Int:ViewModel] = [:]
      
      // currently active aka selected aka key window
      @Published var activeWindow: NSWindow?
      
      // currently active view model for the active window
      @Published var activeViewModel: ViewModel?
      
      func addWindow(window: NSWindow) {
        window.delegate = self
        windows.insert(window)
      }
      
      // associates a window number with a view model
      func addViewModel(_ viewModel: ViewModel, forWindowNumber windowNumber: Int) {
        viewModels[windowNumber] = viewModel
      }
    }
    
    然后,对窗口上的每个更改使用react(当它关闭时以及当它变成事件的又名关键窗口时):
    import SwiftUI
    
    extension GlobalViewModel : NSWindowDelegate {
      func windowWillClose(_ notification: Notification) {
        if let window = notification.object as? NSWindow {
          windows.remove(window)
          viewModels.removeValue(forKey: window.windowNumber)
          print("Open Windows", windows)
          print("Open Models", viewModels)
        }
      }
      func windowDidBecomeKey(_ notification: Notification) {
        if let window = notification.object as? NSWindow {
          print("Activating Window", window.windowNumber)
          activeWindow = window
          activeViewModel = viewModels[window.windowNumber]
        }
      }
    }
    
    提供一种查找与当前 View 关联的窗口的方法:
    import SwiftUI
    
    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) {}
    }
    
    这是使用当前窗口和 viewModel 更新全局 View 模型的 View :
    import SwiftUI
    
    struct ContentView: View {
      @EnvironmentObject var globalViewModel : GlobalViewModel
      
      @StateObject var viewModel: ViewModel  = ViewModel()
      
      var body: some View {
        HostingWindowFinder { window in
          if let window = window {
            self.globalViewModel.addWindow(window: window)
            print("New Window", window.windowNumber)
            self.globalViewModel.addViewModel(self.viewModel, forWindowNumber: window.windowNumber)
          }
        }
        
        TextField("", text: $viewModel.inputText)
          .disabled(true)
          .padding()
      }
    }
    
    然后我们需要创建全局 View 模型并将其发送到 View 和命令:
    import SwiftUI
    
    @main
    struct multi_window_menuApp: App {
      
      @State var globalViewModel = GlobalViewModel()
      
      var body: some Scene {
        WindowGroup {
          ContentView()
            .environmentObject(self.globalViewModel)
        }
        .commands {
          MenuCommands(globalViewModel: self.globalViewModel)
        }
        
        Settings {
          VStack {
            Text("My Settingsview")
          }
        }
      }
    }
    
    以下是命令的外观,因此它们可以访问当前选择的/事件的 viewModel:
    import Foundation
    import SwiftUI
    import Combine
    
    struct MenuCommands: Commands {
      var globalViewModel: GlobalViewModel
      
      var body: some Commands {
        CommandGroup(after: CommandGroupPlacement.newItem, addition: {
          Divider()
          Button(action: {
            let dialog = NSOpenPanel();
            
            dialog.title = "Choose a file";
            dialog.showsResizeIndicator = true;
            dialog.showsHiddenFiles = false;
            dialog.allowsMultipleSelection = false;
            dialog.canChooseDirectories = false;
            
            if (dialog.runModal() ==  NSApplication.ModalResponse.OK) {
              let result = dialog.url
              if (result != nil) {
                let path: String = result!.path
                do {
                  let string = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8)
                  print("Active Window", self.globalViewModel.activeWindow?.windowNumber)
                  self.globalViewModel.activeViewModel?.inputText = string
                }
                catch {
                  print("Error \(error)")
                }
              }
            } else {
              return
            }
          }, label: {
            Text("Open File")
          })
          .keyboardShortcut("O", modifiers: [.command])
        })
      }
    }
    
    在这个 github 项目下所有更新和运行:https://github.com/ondrej-kvasnovsky/swiftui-multi-window-menu

    关于swift - 如何在 macOS 的 SwiftUI 中使用菜单命令实现多窗口?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68242439/

    相关文章:

    android - 在 iOS 中,我们在 Android 上是否有类似 Gradle Build Flavors 的东西

    macos - 升级到 macOS Catalina 后找不到 Conda

    swift - MacO 上状态的保存和恢复

    swift - 充满活力的背景中的标签和 NSView

    ios - 如何在: class NetworkManager: ObservableObject内引用@Published var

    swift - TabView 的 TabItem 的选定选项卡图像在 SwiftUI 中始终为蓝色

    swift - 使用 Coder 初始化 PFTableViewCell

    ios - 使用代码更新 Storyboard约束

    ios - 在范围内找不到类型 'Content'

    ios - 问题限制十进制长度