← Back to list

Memento Pattern [Swift]

There are many ways to implement the Memento pattern, but how many of them are actually faithful to the pattern?

Kirill Chuyanov · 2025-08-14 15:25 · 44 claps · 7.4 min read paywalled
#swift #software-design-patterns #memento-pattern #app-development #swiftui
Open on Medium ↗
Wiki topics: CUL · Culture & Media 📱 · Mobile Development 😂 · Humor & Satire 🕊️ · Religion

Memento Pattern [Swift]

Photo by Teo D on Unsplash

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

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

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.

[embed]Domain-Driven Design in iOS - Learn DDD, SwiftUI & CQRS for Master Domain-Driven Design in iOS. Learn DDD, SwiftUI, SwiftData & CQRS to build scalable mobile apps-100% local, no…codedriven.thinkific.com


메타데이터
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