如何在macOS SwiftUI中用FileImporter实现多窗口独立文件展示
问题描述
当前macOS SwiftUI应用存在以下情况:
- 全局
@State var modelData初始化为空结构体时,ContentView无内容显示(符合预期) - 通过「File > Open File」导入文件后,
modelData更新,当前窗口展示新数据(符合需求) - 使用「File > New Window」打开新窗口时,新窗口与现有窗口内容一致,原因是共享了全局的
modelData - 尝试将
modelData移至ContentView或WindowGroup内部,但无法在.commands中访问;在ContentView环境中初始化新ModelData会导致状态与视图断开无法更新
解决方案
核心思路是让每个窗口拥有独立的状态实例,同时通过FocusedSceneValue让App级命令能访问当前激活窗口的状态:
- 创建
WindowViewModel(ObservableObject)管理单个窗口的modelData和文件路径 - 在
WindowGroup中为每个窗口初始化独立的WindowViewModel,并通过focusedSceneValue暴露给命令 - 在App级命令中,通过
FocusedSceneValue获取当前激活窗口的ViewModel,处理文件导入并更新对应窗口的状态
完整代码
import SwiftUI import UniformTypeIdentifiers // 单个窗口的状态管理器,负责管理当前窗口的model数据和文件路径 class WindowViewModel: ObservableObject { @Published var modelData = ModelData() @Published var openedFilePath = "" } // 定义FocusedSceneValue的Key,用于在命令中访问当前激活窗口的ViewModel private struct WindowViewModelKey: FocusedSceneValueKey { typealias Value = WindowViewModel } extension FocusedSceneValues { var windowViewModel: WindowViewModel? { get { self[WindowViewModelKey.self] } set { self[WindowViewModelKey.self] = newValue } } } @main struct DataVisualizerApp: App { @State private var showFileImporter = false // 通过FocusedSceneValue获取当前激活窗口的ViewModel实例 @FocusedSceneValue(\.windowViewModel) private var focusedWindowViewModel @AppStorage(\.fontSize) var fontSize var body: some Scene { WindowGroup { // 每个窗口初始化独立的WindowViewModel,保证窗口状态隔离 let viewModel = WindowViewModel() ContentView() .environmentObject(viewModel) .focusedSceneValue(\.windowViewModel, viewModel) .environment(\.font, Font.system(size: fontSize)) .navigationTitle { Text("Data Visualizer") if !viewModel.openedFilePath.isEmpty { Text(" - \(viewModel.openedFilePath)") } } } .commands { CommandGroup(before: CommandGroupPlacement.newItem) { Button("Open File") { showFileImporter = true } .fileImporter( isPresented: $showFileImporter, allowedContentTypes: [.data] ) { result in guard let viewModel = focusedWindowViewModel else { return } switch result { case .success(let file): viewModel.openedFilePath = file.path viewModel.modelData = ModelData(securityScopedURL: file) case .failure(let error): print(error) } } // 无激活窗口时禁用按钮 .disabled(focusedWindowViewModel == nil) } } } } // ContentView通过@EnvironmentObject获取当前窗口的ViewModel struct ContentView: View { @EnvironmentObject private var viewModel: WindowViewModel var body: some View { // 这里根据viewModel.modelData渲染具体内容 Text(viewModel.openedFilePath.isEmpty ? "No file opened" : "Opened: \(viewModel.openedFilePath)") } } // 示例ModelData结构体,根据实际业务实现初始化逻辑 struct ModelData { init() {} init(securityScopedURL: URL) { // 实现从URL加载数据的逻辑 } } // 示例AppStorage键定义 extension AppStorage where Value == Double { static var fontSize: Self { Self("fontSize", defaultValue: 14) } }
关键说明
- 每个
WindowGroup实例会创建独立的WindowViewModel,新窗口默认是空状态 - 「Open File」命令仅更新当前激活窗口的ViewModel,不会影响其他窗口
- 通过
@EnvironmentObject让ContentView能响应ViewModel的状态变化 focusedSceneValue确保命令只与当前激活的窗口交互
内容的提问来源于stack exchange,提问作者TT--
相关产品推荐
相关产品推荐

