你是否曾在 iOS App 製作「復原」按鈕時遇到瓶頸?
在文字編輯器 App 中,若要讓使用者復原剛才的編輯,首先就得思考如何保留先前的狀態。
如果在各處備份物件內部的屬性,程式碼很快就會變得凌亂。
這時最適合的就是Swift Memento Pattern。今天會搭配程式碼,分享我實際使用這個模式優雅儲存與還原物件狀態的經驗。
Memento Pattern 是一種設計模式,能在不破壞封裝的情況下,將物件的內部狀態儲存到外部,之後再還原成該狀態。
一句話說,就是「先拍下快照」。復原(Undo)、遊戲存檔、表單暫存等功能都建立在這個概念上。
什麼是 Memento Pattern?先從三種角色開始 🧩
Memento Pattern 的角色剛好有三個。理解這裡,就完成一半了。
- Originator(原始物件):持有狀態的主角。將自己的狀態儲存成 Memento,也能透過 Memento 還原。
- Memento(備忘錄):包含特定時間點狀態的快照。建立後內容最好不要再變更。
- Caretaker(管理者):負責保管 Memento。通常就是復原堆疊。
重點在於,管理者不會查看 Memento「內部」的內容。
管理者只會接收快照並堆疊保存,需要時再交還。原始物件的內部結構只有原始物件自己知道。
因此不會破壞封裝,這正是 Memento Pattern 最大的魅力。
如何使用 Swift 實作 Memento Pattern?
以簡單的文字編輯器為例。先定義 Memento 與原始物件。
下面是保存狀態的 Memento,以及建立並還原該 Memento 的原始物件。
// 狀態快照 — 建立後保持不變 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)功能 ⏪
接著加入管理者,也就是復原堆疊,負責逐一保存前面建立的 Memento。
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
許多介紹 Memento Pattern 的文章,原本都是以 Java 等語言為前提。不過在 Swift 中善用語言特性,實作會方便許多。
第一,Memento 比起class更適合使用struct。因為是值型別,複製的瞬間就會成為獨立的快照,也能減少參考互相牽連而導致原始狀態被修改的疑慮。
第二,讓 Memento 採用Codable後,就容易將狀態儲存到檔案或 UserDefaults,也能擴充成關閉並重新開啟 App 後仍保留的存檔功能。
第三,如果使用 UIKit,建議先了解現成的UndoManager系統類別。自行實作前,先掌握標準工具才是正確順序。
常見問題(Q&A)
Q. 直接備份到變數裡不行嗎?
簡單的情況可以。但當狀態變多,或儲存/還原邏輯變複雜時,散落各處的備份程式碼就會成為 Bug 的溫床。Memento Pattern 能將它集中在同一處。
Q. 不會使用太多記憶體嗎?
沒錯。無限堆疊快照會增加記憶體負擔。因此實務上常將歷史記錄限制在約 20~50 筆,或只儲存有變更的部分來最佳化。
Memento Pattern 只是名稱陌生,實際上就是「拍下快照並還原」這個簡單概念。
如果你正在考慮復原或暫存功能,不妨先跟著今天的範例程式碼實作一次。你會很快掌握在維持封裝的同時處理狀態的感覺。加油!😊

