Memento Pattern [Swift]
There are many ways to implement the Memento pattern, but how many of them are actually faithful to the pattern?
Memento Pattern [Swift]
Photo by Teo D on Unsplash
Recently, I came across a post on LinkedIn with a snippet on how to implement the Memento Pattern:

A mere glimpse of this snippet was enough to realize that this implementation is far from being the Memento Pattern.
Through building a simple macOS text editor app, we will figure out:
- What is the Memento Pattern?
- What pattern comes along with it?
- How to implement it properly?
I am a fan of the GoF book, but there is a lovely website with examples in many languages that contains all the patterns from the book and more.
What is the Memento Pattern
The primary purpose of the pattern is to encapsulate an object’s internal state, allowing it to be restored later without exposing its implementation details.
The pattern consists of:
- Memento
- Originator
- Caretaker

https://refactoring.guru/design-patterns/memento
Memento
The Memento stores the internal state of the Originator object, and ideally, to maintain encapsulation, it should not expose the internal state directly.
Originator
It mutates its state and knows how to save it and how to restore it through Memento.
Caretaker
It is responsible for keeping mementos and should not operate on its internal content.
As you can see, in the original post, only the caretaker is depicted. At the same time, the central part of the pattern — Memento — is taken away via generics, and nothing is mentioned about the source of the state.
Command Pattern
In the post, my first comment addressed the assumption that the Command Pattern is being implemented, rather than the Memento Pattern. But I did not pay enough attention to it at that time.
So why did I think so?
commit, undo, and redo are commands to be executed to mutate the state. However, upon closer examination of the implementation details, it is just methods of the in-memory storage and nothing more.
There is another pattern that brings clarity and extensibility to the problem — Command Pattern.

https://refactoring.guru/design-patterns/command
Although the diagram appears complex, it is straightforward to implement, and many developers have utilized it at some point without even realizing it. We’ll see how to implement it below.
Proper implementation
In a simple Text Editor app, we will start from the originator to understand what state we are about to preserve:
@Observable
final class EditorViewModel {
var text = ""
var selection = NSRange(location: 0, length: 0)
}
We need a text to store and a selection range to keep in case of cut-and-paste. Next, let’s define the snapshot (aka Memento):
struct EditorSnapshot {
fileprivate let text: String
fileprivate let selection: NSRange
}
As you can see, the properties of the snapshot are fileprivate, so only the view model has access to it. At the same time, it implies that the snapshot lives in the same file. Let’s now follow the pattern and extend the view model:
final class EditorViewModel {
//...
func createSnapshot() -> EditorSnapshot {
EditorSnapshot(text: text, selection: selection)
}
func restore(_ snapshot: EditorSnapshot) {
text = snapshot.text
selection = snapshot.selection
}
}
Easy as that.
View
Before diving into the last part of the pattern — caretaker — let’s implement the view and define what commands it supports.
struct EditorView: View {
@State private var viewModel = EditorViewModel()
var body: some View {
CocoaTextView(text: $viewModel.text, selection: $viewModel.selection)
.frame(minWidth: 520, minHeight: 340)
.padding()
}
}
@main
struct MementoInActionMacOSApp: App {
var body: some Scene {
WindowGroup {
EditorView()
}
}
}
This is the basic implementation that will display the text editor, allowing users to undo/redo changes using the built-in capabilities. We will override it via commands.
Commands
First, to follow the Command Pattern, we need to define the common interface for all commands:
protocol Command {
func execute() -> Bool
func undo()
}
execute here returns Bool to indicate whether the Command has changed the state. It is not the most sophisticated way of checking it, but we’ll stick to it for simplicity. Every executed Command that mutates the state will be stored in the command history, allowing for later undoing and redoing of changes.
Next, let’s define what commands the app supports:
- Cut
- Paste
- Save
Cut:
final class CutCommand: Command {
private let originator: EditorViewModel
private var snapshot: EditorSnapshot?
init(originator: EditorViewModel) {
self.originator = originator
}
func execute() -> Bool {
if snapshot == nil {
snapshot = originator.createSnapshot()
}
let string = originator.text as NSString
let range = originator.selection
guard
range.location != NSNotFound, range.length > 0,
range.location + range.length <= string.length
else { return false }
let slice = string.substring(with: range)
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(slice, forType: .string)
let newText = string.replacingCharacters(in: range, with: "")
originator.text = newText
originator.selection = NSRange(location: range.location, length: 0)
return true
}
func undo() {
snapshot.map(originator.restore)
}
}
From the snippet, we see that a command in the Memento pattern plays the role of the caretaker, as it knows how to retrieve the state and how to restore it. Because the editor supports redo (via re-execution of the command), we need to keep track of the state that existed before the command mutated it. If we do a redo, we won’t create a new snapshot, which would cause damage to the history. The rest is simple — the undo method applies the saved snapshot.
Paste:
final class PasteCommand: Command {
private let originator: EditorViewModel
private var snapshot: EditorSnapshot?
init(originator: EditorViewModel) {
self.originator = originator
}
func execute() -> Bool {
if snapshot == nil {
snapshot = originator.createSnapshot()
}
guard
let paste = NSPasteboard.general.string(forType: .string),
!paste.isEmpty
else { return false }
let ns = originator.text as NSString
let selection = originator.selection.safeRange(inLength: ns.length)
let replaced = ns.replacingCharacters(in: selection, with: paste)
originator.text = replaced
originator.selection = NSRange(location: selection.location + (paste as NSString).length, length: 0)
return true
}
func undo() {
snapshot.map(originator.restore)
}
}
extension NSRange {
func safeRange(inLength len: Int) -> NSRange {
let loc = max(0, min(self.location, len))
let maxLen = max(0, len - loc)
let length = max(0, min(self.length, maxLen))
return NSRange(location: loc, length: length)
}
}
The Paste Command is pretty much the same as the Cut command.
Save:
final class SaveCommand: Command {
private let originator: EditorViewModel
private var after: EditorSnapshot?
private var before: EditorSnapshot?
init(originator: EditorViewModel) {
self.originator = originator
}
func execute() -> Bool {
if let after {
originator.restore(after)
} else {
before = originator.lastSavedSnapshot
after = originator.createSnapshot()
}
originator.lastSavedSnapshot = after
return true
}
func undo() {
originator.restore(before ?? .empty())
originator.lastSavedSnapshot = before
}
}
The Save command is not intended to be here, as it primarily saves checkpoints during the text lifespan rather than on disk. Yet, it is interesting to see how it is implemented.
Because it can be undone, we need to keep the state before and after the command execution. Compared to the commands above, the save command is about accepting that the current state is the mutation, while others first take a snapshot and then mutate the state. To support that, as a simple solution, we introduce lastSavedSnapshot to the view model:
final class EditorViewModelV2 {
// ...
var lastSavedSnapshot: EditorSnapshot?
// ...
}
Commands History
Now, it’s time to implement the command history skeleton so we can finish the setup part and dive into the implementations related to the original post.
@Observable
final class CommandHistory {
private var history: [Command] = []
var canUndo: Bool { false }
var canRedo: Bool { false }
func perform(_ command: Command) {
guard command.execute() else { return }
history.append(command)
}
func undo() {
}
func redo() {
}
}
Next, let’s set up commands in the app menu:
struct EditorCommands: Commands {
@FocusedValue(\.editorModel) var viewModel
@FocusedValue(\.commandHistory) var history
var body: some Commands {
CommandGroup(replacing: .saveItem) {
Button("Save") {
guard let viewModel, let history else { return }
history.perform(SaveCommand(originator: viewModel))
}
.keyboardShortcut("s", modifiers: [.command])
}
CommandMenu("Edit") {
Button("Cut") {
guard let viewModel, let history else { return }
history.perform(CutCommand(originator: viewModel))
}
.keyboardShortcut("x", modifiers: [.command])
Button("Paste") {
guard let viewModel, let history else { return }
history.perform(PasteCommand(originator: viewModel))
}
.keyboardShortcut("v", modifiers: [.command])
}
CommandGroup(replacing: .undoRedo) {
Button("Undo") {
history?.undo()
}
.disabled(history?.canUndo == false)
.keyboardShortcut("z", modifiers: [.command])
Button("Redo") {
history?.redo()
}
.disabled(history?.canRedo == false)
.keyboardShortcut("z", modifiers: [.command, .shift])
}
}
}
The command pattern aligns perfectly with the interface that the macOS SDK provides. It allows us to maintain a clear separation of concerns.
We need to modify the view and provide FocusedValues:
struct EditorView: View {
@State private var viewModel = EditorViewModel()
@State private var history = CommandHistory()
var body: some View {
CocoaTextView(text: $viewModel.text, selection: $viewModel.selection)
.frame(minWidth: 520, minHeight: 340)
.padding()
.focusedSceneValue(\.editorModel, viewModel)
.focusedSceneValue(\.commandHistory, history)
}
}
private struct EditorModelKey: FocusedValueKey { typealias Value = EditorViewModel }
private struct CommandHistoryKey: FocusedValueKey { typealias Value = CommandHistory }
extension FocusedValues {
var editorModel: EditorViewModel? {
get { self[EditorModelKey.self] }
set { self[EditorModelKey.self] = newValue }
}
var commandHistory: CommandHistory? {
get { self[CommandHistoryKey.self] }
set { self[CommandHistoryKey.self] = newValue }
}
}
Next, we need to attach EditorCommands to the view:
@main
struct MementoInActionMacOSApp: App {
var body: some Scene {
WindowGroup {
EditorView()
}.commands {
EditorCommands()
}
}
}
The implementation
Actually, I like the idea of the cursor from the original LinkedIn post, but the implementation had a bug, so I fixed it here:
@Observable
final class CommandHistory {
var history: [Command] = []
private var cursor: Int = -1
var canUndo: Bool { cursor >= 0 }
var canRedo: Bool { cursor < history.count - 1 }
func perform(_ command: Command) {
guard command.execute() else { return }
history = Array(history.prefix(cursor + 1))
history.append(command)
cursor += 1
}
func undo() {
guard canUndo else { return }
history[cursor].undo()
cursor -= 1
}
func redo() {
guard canRedo else { return }
cursor += 1
_ = history[cursor].execute()
}
}
Thus, in the original code snippet, if the history consists of a single state change, after performing the undo operation, redo will return nil, while the state will be in the stack.
Summarize
I can’t imagine a short post of such patterns, and misrepresenting a small part as the entire pattern is also incorrect. Memento + Command are patterns that usually come hand in hand and complement each other well. However, to use them effectively, it’s essential to understand their roles and how they interact within the context of your application.
In my course, I demonstrate another practical application of the Memento pattern, which enables the easy storage of domain objects without making them dependent on a backing storage library.
Join it now and get free updates.
메타데이터
- post_id
- a203dd52d5fb
- slug
- memento-pattern-swift-a203dd52d5fb
- url
- https://medium.com/@archanger/memento-pattern-swift-a203dd52d5fb
- canonical_url
- https://medium.com/@archanger/memento-pattern-swift-a203dd52d5fb
- author_url
- https://medium.com/@archanger
- status
- ok
- fetched_at
- 2026-07-25 22:50:20