软件设计

Swift 备忘录模式完整指南(撤销功能实现示例)

你是否曾在 iOS 应用中制作“撤销”按钮时遇到瓶颈?

4 分钟阅读
Swift 备忘录模式完整指南(撤销功能实现示例) 封面图

你是否曾在 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,避免保存时的状态之后被修改。

只要分开 save 和 restore,代码就能变得这么简洁
只要分开 save 和 restore,代码就能变得这么简洁

实战:实现撤销(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 条,或只保存发生变化的部分进行优化。


备忘录模式只是名字陌生,实际上就是“拍快照并恢复”这一简单思想。

如果你正在考虑撤销或临时保存功能,不妨先跟着今天的示例代码敲一遍。你会很快掌握在保持封装的同时处理状态的感觉。加油!😊

延伸阅读