开发应用时,“只加一个撤销按钮”这样的需求,出现得比想象中更频繁。
真正接入 undo 后,代码很容易变成意大利面。因为仅仅为了一个按钮,就要把撤销逻辑分散到删除、移动和修改代码的各个位置。
解决这一问题的正是Swift 命令模式。
本文将介绍命令模式为什么能成为撤销功能的基础,并讲解实际使用的最小代码结构。重点不是复杂理论,而是“可以立即接入项目的形式”。
什么是命令模式?一行总结
先用一句话概括。
命令模式把“执行某个操作”本身封装成对象,将执行和撤销组合成一个单元。
按下按钮时,不直接执行逻辑,而是创建一个写着“执行此操作”的命令(Command 对象)。
命令中同时包含执行方式(execute)和恢复方式(undo)。
总结如下:
- 将操作创建为对象——删除、移动和输入分别对应一个命令
- 每个命令成对拥有 execute() 和 undo()
- 将执行过的命令压入栈
- 从栈中取出一个命令并调用 undo(),撤销就完成了
借助这种结构,撤销逻辑不会散落在各处,而会紧跟在各个命令内部。
用 Swift 从命令协议开始构建
首先定义所有命令都必须遵守的约定,也就是协议。
下面是强制执行和撤销的最小骨架。
protocol Command {
func execute() // 执行操作
func undo() // 撤销执行
}
只需要这两个方法。
现在创建一个包含实际操作的具体命令。例如,实现添加文本的命令。
下面的代码会“添加文字,撤销时再将其移除”。
final class AddTextCommand: Command {
private let document: Document
private let text: String
init(document: Document, text: String) {
self.document = document
self.text = text
}
func execute() { document.content += text }
func undo() { document.content.removeLast(text.count) }
}
execute 负责添加文字,undo 负责移除添加的内容。执行和撤销位于同一个对象中,这就是该模式的全部。
撤销的核心是栈
创建命令后,还需要一个负责管理它们的“管理器”。这个角色通常称为 Invoker。
管理器会将执行过的命令依次压入栈中。
点击撤销时,取出最近压入的命令并调用 undo()。先撤销最后执行的操作,这就是撤销的自然顺序。
下面是管理器的骨架。
final class CommandManager {
private var undoStack: [Command] = []
func run(_ command: Command) {
command.execute()
undoStack.append(command) // 执行后保存到栈中
}
func undo() {
guard let last = undoStack.popLast() else { return }
last.undo() // 从最后一项开始撤销
}
}
现在的用法就非常简单了。
在 buttonTapped 中调用 manager.run(command),在撤销按钮中只调用 manager.undo() 即可。
撤销顺序和栈管理由 CommandManager 自动处理,因此界面代码可以保持整洁。
如果还需要重做(redo),可以将撤销的命令单独保存到 redoStack 中。由于结构是对称的,扩展起来并不困难。
命令模式什么时候该用,什么时候该避免?
命令模式也不是万能的。下面用表格整理了适用和过度设计的场景。
| 场景 | 命令模式 |
|---|---|
| 需要撤销和重做 | 非常适合 |
| 需要保留操作记录(历史) | 适合 |
| 将操作放入队列稍后执行 | 适合 |
| 无需撤销的简单按钮 | 过度设计,直接用函数 |
命令模式在需要“撤销”或“记录”时最有价值。画图软件、文本编辑器、支付取消和游戏中的撤销功能,都是典型案例。
相反,如果只是给页面跳转按钮套用命令模式,反而只会增加代码。
把所有按钮都做成命令,很容易增加大量文件,因此只应应用于需要撤销的操作。
总结
撤销功能看似难以接入,但只要先搭好命令模式这个基础,就能比想象中更整洁地运行。
先把今天整理的协议、具体命令和管理器三个部分接入一个小页面。熟悉结构后,扩展 redo 或历史记录也会顺理成章。现在就自信地为你的应用加上撤销按钮吧。

