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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 23:17:22