你是否曾在 iOS 应用中制作“撤销”按钮时遇到瓶颈?
在文本编辑器应用中,要让用户撤销刚才的编辑,首先就要考虑如何保留之前的状态。
如果在各处备份对象的内部属性,代码很快就会变得凌乱。
这时最合适的就是Swift 备忘录模式。今天我会结合代码,分享自己使用这一模式优雅保存和恢复对象状态的经验。
备忘录模式会在不破坏封装的情况下,将对象的内部状态保存到外部,并在之后恢复到该状态。
一句话概括,就是“先拍一张快照”。撤销(Undo)、游戏存档、表单临时保存等功能都以此为基础。
什么是备忘录模式?先从三个角色开始 🧩
备忘录模式中恰好有三个角色。理解这里,就完成一半了。
- Originator(原发器):持有状态的主角。它将自身状态保存为备忘录,并通过备忘录进行恢复。
- Memento(备忘录):包含特定时刻状态的快照。理想情况下,创建后其内容不应改变。
- Caretaker(管理者):负责保存备忘录。通常由撤销栈承担这一角色。
关键在于,管理者不会查看备忘录的“内部”。
管理者只负责接收并堆叠保存快照,需要时再交回。原发器的内部结构只有原发器自己知道。
因此不会破坏封装,这正是备忘录模式最大的魅力。
如何用 Swift 实现备忘录模式?
下面以一个简单的文本编辑器为例。首先定义备忘录和原发器。
下面是保存状态的备忘录,以及创建并恢复该备忘录的原发器。
// 状态快照——创建后保持不变 let
struct EditorMemento {
let text: String
}
final class TextEditor {
var text: String = ""
func save() -> EditorMemento { EditorMemento(text: text) } // 保存
func restore(_ memento: EditorMemento) { text = memento.text } // 恢复
}
save()会将当前状态拍成快照,restore(_:)则会恢复到收到的快照。
关键是将EditorMemento的属性设为let,避免保存时的状态之后被修改。
实战:实现撤销(Undo)功能 ⏪
现在加入管理者,也就是撤销栈,负责逐个保存之前创建的备忘录。
final class UndoManager {
private var history: [EditorMemento] = []
func backup(_ memento: EditorMemento) { history.append(memento) }
func undo() -> EditorMemento? { history.popLast() } // 取出最近的状态
}
实际流程如下。
在用户修改文字之前,先通过backup保存当前状态。
用户点击“撤销”后,undo取出最后一个快照,原发器再使用它进行restore。
实际接入后,编辑逻辑和保存逻辑清晰分离,代码整洁了很多。
即使混合了添加文字、删除文字、修改格式等多种编辑,保存方式仍然统一,维护起来更方便。
Swift 风格的使用技巧:活用 Codable 和 struct
许多文章都是以 Java 等语言为前提介绍备忘录模式的。不过在 Swift 中,利用语言特性会方便得多。
第一,struct比class更适合备忘录。因为它是值类型,复制的瞬间就会成为独立的快照,也能减少因引用纠缠而导致原始状态被修改的风险。
第二,让备忘录采用Codable后,就容易将状态保存到文件或 UserDefaults,还可以扩展为关闭并重新打开应用后仍然保留的存档功能。
第三,如果使用 UIKit,建议先了解现成的UndoManager系统类。自行实现之前,先掌握标准工具才是正确顺序。
常见问题(Q&A)
Q. 直接备份到变量里不行吗?
简单情况下可以。但当状态较多,或保存与恢复逻辑变复杂时,分散的备份代码就会成为 Bug 的温床。备忘录模式可以将其集中到一处。
Q. 不会占用太多内存吗?
确实如此。无限堆叠快照会增加内存负担。因此实际开发中通常会将历史记录限制在约 20~50 条,或只保存发生变化的部分进行优化。
备忘录模式只是名字陌生,实际上就是“拍快照并恢复”这一简单思想。
如果你正在考虑撤销或临时保存功能,不妨先跟着今天的示例代码敲一遍。你会很快掌握在保持封装的同时处理状态的感觉。加油!😊

