使用TCA结合DocumentGroup时的文档同步与Tab切换异常问题
在macOS中结合DocumentGroup与Composable Architecture(TCA)的正确实践
核心问题分析
你当前的实现直接将文档的Binding传入视图并通过.onChange修改文档属性,会触发DocumentGroup的全量重构——因为文档是Scene的核心数据源,修改后整个Scene会重新初始化,导致标签页重置回默认项。同时这种方式也没有利用TCA的状态管理优势,反而引入了状态同步的混乱。
解决方案步骤
1. 重构文档与TCA的状态关系
让文档模型作为TCA状态的数据源,同时将文档标记为ObservableObject,所有状态变更通过TCA的Action和Reducer处理,避免直接操作文档的Binding。
2. 手动标记文档变更
在TCA的Effect中更新文档内容后,手动设置document.hasUnsavedChanges = true,让macOS系统识别到变更并启用保存按钮,无需依赖@Published的自动刷新触发Scene重构。
3. 托管标签页状态
将当前选中的标签页索引存入TCA的State中,确保即使Scene有局部刷新,也能自动恢复到之前选中的标签。
代码示例
文档模型定义
import SwiftUI import UniformTypeIdentifiers struct Item: Identifiable, Codable, Equatable { let id: UUID var name: String } extension UTType { static let itemDocument = UTType(exportedAs: "com.yourdomain.item-document") } class MyDocument: NSDocument, ObservableObject { @Published var items: [Item] = [] override static var readableContentTypes: [UTType] { [.itemDocument] } override static var writableContentTypes: [UTType] { [.itemDocument] } override func makeWindowControllers() { // 基于文档初始数据创建TCA Store let store = Store(initialState: AppFeature.State(items: items, selectedTab: 0)) { AppFeature() ._printChanges() // 调试时查看状态变更 } let hostingController = NSHostingController(rootView: AppView(store: store, document: self)) let window = NSWindow(contentViewController: hostingController) let windowController = NSWindowController(window: window) addWindowController(windowController) } override func write(to url: URL, ofType typeName: String) throws { let data = try JSONEncoder().encode(items) try data.write(to: url) } override func read(from url: URL, ofType typeName: String) throws { let data = try Data(contentsOf: url) items = try JSONDecoder().decode([Item].self, from: data) } }
TCA Feature定义
import ComposableArchitecture struct AppFeature: Reducer { struct State: Equatable { var items: [Item] var selectedTab: Int = 0 } enum Action: Equatable { case addItem case selectTab(Int) case syncItemsToDocument([Item]) case syncItemsFromDocument([Item]) } // 将文档注入环境,避免在Reducer中通过视图层级获取 struct Environment { weak var document: MyDocument? } @Dependency(\.self) var environment func reduce(into state: inout State, action: Action) -> Effect<Action> { switch action { case .addItem: state.items.append(Item(id: UUID(), name: "新建项目")) return .send(.syncItemsToDocument(state.items)) case .selectTab(let index): state.selectedTab = index return .none case .syncItemsToDocument(let items): return .run { [items] send in guard let document = environment.document else { return } document.items = items document.hasUnsavedChanges = true // 手动标记变更 } case .syncItemsFromDocument(let items): state.items = items return .none } } }
主视图实现
struct AppView: View { let store: StoreOf<AppFeature> weak var document: MyDocument? init(store: StoreOf<AppFeature>, document: MyDocument) { self.store = store self.document = document // 将文档注入TCA环境 store.environment = AppFeature.Environment(document: document) } var body: some View { WithViewStore(store) { viewStore in TabView(selection: viewStore.binding( get: \.selectedTab, send: AppFeature.Action.selectTab )) { Text("标签1内容") .tabItem { Text("标签1") } .tag(0) VStack { List(viewStore.items) { item in Text(item.name) } Button("添加项目") { viewStore.send(.addItem) } .padding() } .tabItem { Text("标签2") } .tag(1) } // 监听文档外部变更(比如其他编辑器修改),同步到TCA状态 .onReceive(document?.$items.dropFirst() ?? Empty()) { newItems in viewStore.send(.syncItemsFromDocument(newItems)) } } } }
关键注意事项
- 所有状态变更都通过TCA的Action流转,避免直接操作文档的
@Published属性导致Scene全量刷新。 - 手动设置
hasUnsavedChanges既可以让系统识别到变更,又不会触发不必要的视图重构。 - 将文档注入TCA环境,让Reducer可以直接访问文档,避免通过视图层级传递的耦合问题。
内容的提问来源于stack exchange,提问作者appfrosch
相关产品推荐
相关产品推荐

