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

如何在iOS17 Beta4+中用SwiftData实现SwiftUI的撤销/重做功能

SwiftData实体编辑表单实现自定义撤销/重做按钮

问题核心

系统自带的撤销(摇一摇、三指滑动)是文本控件级别的局部撤销,而你需要的是针对托管对象字段修改的完整撤销——即每次修改字段后记录原始状态,点击按钮可恢复该字段的初始值,而非仅删除输入的部分字符。

实现方案

方案1:手动维护快照栈(精准控制字段级撤销)

适合需要完全自定义撤销范围的场景,比如只跟踪指定字段的修改。

1. 定义实体快照类

用于保存修改前的字段状态:

import SwiftData

class TradingEntitySnapshot {
    let entityId: UUID
    let originalFields: [String: Any]
    
    init(entity: TradingEntity) {
        self.entityId = entity.id
        // 按你的实体字段,手动配置需要跟踪的属性
        self.originalFields = [
            "name": entity.name ?? "",
            "amount": entity.amount,
            "price": entity.price
            // 补充其他需要监控的字段
        ]
    }
}

2. 在编辑器视图中管理撤销/重做栈

struct TradingEntityEditor: View {
    @Bindable var entity: TradingEntity
    @Environment(\.modelContext) private var modelContext
    
    private var undoStack: [TradingEntitySnapshot] = []
    private var redoStack: [TradingEntitySnapshot] = []
    
    var body: some View {
        Form {
            TextField("名称", text: $entity.name)
                .onChange(of: entity.name) { oldVal, newVal in
                    guard oldVal != newVal else { return }
                    // 若当前字段已有未撤销的修改,替换快照避免重复
                    if let last = undoStack.last, last.originalFields["name"] != nil {
                        undoStack.removeLast()
                    }
                    undoStack.append(TradingEntitySnapshot(entity: entity))
                    redoStack.removeAll() // 新修改清空重做栈
                }
            
            TextField("金额", value: $entity.amount, format: .number)
                .onChange(of: entity.amount) { oldVal, newVal in
                    guard oldVal != newVal else { return }
                    if let last = undoStack.last, last.originalFields["amount"] != nil {
                        undoStack.removeLast()
                    }
                    undoStack.append(TradingEntitySnapshot(entity: entity))
                    redoStack.removeAll()
                }
            
            // 其他字段同理配置...
            
            HStack {
                Button("撤销") {
                    guard let lastSnapshot = undoStack.popLast() else { return }
                    // 保存当前状态到重做栈
                    redoStack.append(TradingEntitySnapshot(entity: entity))
                    // 恢复快照值
                    restoreEntity(from: lastSnapshot)
                }
                .disabled(undoStack.isEmpty)
                
                Button("重做") {
                    guard let lastRedo = redoStack.popLast() else { return }
                    undoStack.append(TradingEntitySnapshot(entity: entity))
                    restoreEntity(from: lastRedo)
                }
                .disabled(redoStack.isEmpty)
            }
        }
    }
    
    private func restoreEntity(from snapshot: TradingEntitySnapshot) {
        guard snapshot.entityId == entity.id else { return }
        // 逐个恢复字段
        if let name = snapshot.originalFields["name"] as? String {
            entity.name = name
        }
        if let amount = snapshot.originalFields["amount"] as? Double {
            entity.amount = amount
        }
        if let price = snapshot.originalFields["price"] as? Double {
            entity.price = price
        }
        try? modelContext.save()
    }
}

方案2:利用SwiftData原生UndoManager(快速实现)

直接复用SwiftData内置的撤销管理器,适合不需要精细控制的场景:

struct TradingEntityEditor: View {
    @Bindable var entity: TradingEntity
    @Environment(\.modelContext) private var modelContext
    
    var body: some View {
        Form {
            // 表单字段...
            
            HStack {
                Button("撤销") {
                    modelContext.undoManager?.undo()
                }
                .disabled(!(modelContext.undoManager?.canUndo ?? false))
                
                Button("重做") {
                    modelContext.undoManager?.redo()
                }
                .disabled(!(modelContext.undoManager?.canRedo ?? false))
            }
        }
        .onAppear {
            // 启用ModelContext的撤销管理器
            modelContext.undoManager = UndoManager()
        }
    }
}

注意:此方式会跟踪ModelContext中所有实体的修改,若需仅针对当前编辑实体,可结合undoManager?.registerUndo(withTarget:handler:)自定义撤销动作。

关键注意事项

  • 手动快照方案中,需避免同字段连续修改时重复添加快照,确保每次撤销只恢复一次字段修改。
  • 使用原生UndoManager时,若出现撤销层级不符预期,可检查是否有其他ModelContext操作干扰了撤销栈。

内容的提问来源于stack exchange,提问作者apps2go

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 08:28:20