如何在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
相关产品推荐
相关产品推荐

