如何在 macOS 的 SwiftUI 中通过菜单命令实现多窗口?

问题描述 投票:0回答:3

情况

实现多窗口应用程序,其中每个窗口都有自己的状态。

示例

这是一个展示问题的示例(在 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...")
    }
  }
}

问题

我们应该如何以编程方式找出当前选定的视图是什么,以便我们可以在菜单命令即将完成时更新状态并更新视图模型中的状态?

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

可能有助于解决这个问题的链接:

swift macos swiftui
3个回答
8
投票

我在解决类似问题时遇到了这个问题。我相信 SwiftUI 的方式是使用

FocusedValue
:

// create an active viewmodel key
struct ActiveViewModelKey: FocusedValueKey {
  typealias Value = ViewModel
}

extension FocusedValues {
  var activeViewModel: ViewModel? {
    get { self[ActiveViewModelKey.self] }
    set { self[ActiveViewModelKey.self] = newValue }
  }
}

struct ContentView: View {
  @StateObject var viewModel: ViewModel  = ViewModel()
  
  var body: some View {
    TextField("", text: $viewModel.inputText)
      ...
      .focusedSceneValue(\.activeViewModel, viewModel) // inject the focused value
  }
}

struct MenuCommands: Commands {
  @FocusedValue(\.activeViewModel) var activeViewModel // inject the active viewmodel
  
  var body: some Commands {
    CommandGroup(after: CommandGroupPlacement.newItem, addition: {
      Divider()
      Button(action: {
        ...
        activeViewModel?.inputText = string
      }, label: {
        Text("Open File")
      })
      .keyboardShortcut("O", modifiers: [.command])
    })
  }
}

4
投票

有用的链接:

  1. 如何仅使用 SwiftUI 从@main App 访问 NSWindow?
  2. 如何在 SwiftUI 视图中访问自己的窗口?
  3. https://lostmoa.com/blog/ReadingTheCurrentWindowInANewSwiftUILifecycleApp/

(这是我能想到的,如果有人有更好的想法/方法,请分享)

这个想法是创建一个共享的“全局”视图模型,用于跟踪打开的窗口和视图模型。每个

NSWindow
都有一个具有唯一
windowNumber
的属性。当窗口变为活动状态(键)时,它会通过
windowNumber
查找视图模型并将其设置为
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
  }
}

然后,对窗口上的每个更改做出反应(当它被关闭时以及当它成为活动的又名关键窗口时):

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

提供一种查找与当前视图关联的窗口的方法:

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 更新全局视图模型的视图:

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

然后我们需要创建全局视图模型并将其发送到视图和命令:

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

命令如下所示,以便它们可以访问当前选定/活动的视图模型:

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


0
投票

.focusedSceneObject(viewModel) 是 SwiftUI 方式

struct RootView: View {
    @StateObject private var viewModel = ViewModel()


    var body: some View {
        NavigationSplitView {
            ...
        }
        .focusedSceneObject(viewModel)
    }
}

您可以在App中获取viewModel

@FocusedObject private var viewModel: ViewModel?
© www.soinside.com 2019 - 2024. All rights reserved.