You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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级命令能访问当前激活窗口的状态:

  1. 创建WindowViewModel(ObservableObject)管理单个窗口的modelData和文件路径
  2. 在WindowGroup中为每个窗口初始化独立的WindowViewModel,并通过focusedSceneValue暴露给命令
  3. 在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--

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 13:53:20