Design Pattern Review - Part 1

Welcome to my Design Pattern Review series. On this series of blog posts I plan on reviewing both consolidated software design patterns as well as lesser known ones (at least to me).
As software developers we're dealing with the intersection of the creative and the established. We're most likely not the first people to meet a software problem that needs solving. We could be meeting it in a language or context never seen before, but in most cases not even that is true. Software in the object-oriented way we know it today has been written for 50+ years. It's older than the first Star Wars and about as old as e-mail. Feel old yet?
Now, I'm not saying there is no room for creating anything new anymore. We still get to decide what we build, in what order, with what materials and how closely we follow the blueprint software design patterns map out to us. But like an architect building a house the end result is many times more likely to be successful if a blueprint is involved.
"A ship on the beach is a lighthouse to the sea." - Dutch proverb via The Mythical Man-Month
Fred Brooks has many great quotes in TMMM, but I think this opening one is a banger. Like all things in life it's better to learn from past experiences – in this case from programmers that came before us – than trying to reinvent where, how and why walls are built each time.
Design patterns are exactly that. They are decades of craft know-how passed forward so we can build sturdier, more maintainable software and focus on the things that make it different from other software and not on what makes it work. Also, by sharing a common understanding of patterns, any other software developer can come into a project and know right away how a part of the codebase is supposed to work.
My plan with this series is sharing my studies of some software design patterns and fomenting discussion with fellow developers so I and others may learn from it.
Patterns presented in this series have been chosen based mainly on their newness to me, with some stablished ones sprinkled here and there because it's important to have your fundamentals right. This is Part 1, with 7 more parts to follow through the end of the year, each covering 1 software design pattern.
I will be relying on the great Refactoring Guru website for core concepts and implementing them myself in Swift for iOS.
A quick overview of pattern types
Patterns can be divided into 3 categories based on their purpose: Creational, Structural and Behavioral. I am going to quote RG here because I think they nailed it:
Creational patterns provide object creation mechanisms that increase flexibility and reuse of existing code.
Structural patterns explain how to assemble objects and classes into larger structures, while keeping these structures flexible and efficient.
Behavioral patterns take care of effective communication and the assignment of responsibilities between objects.
Having got the whys and whats out of the way, let's start with a fresh one.
Memento (Behavioral)
Have you ever implemented a text editor with an undo feature? I haven't. It's such a common feature we might not give two thoughts to how it's built. That's why this pattern caught my attention when I was first browsing RG.
Thinking it through, what does this combo need? First, the editor itself, which SwiftUI gives us in the form of the very descriptive TextEditor. Second, we need to keep a history of some sort of the changes so we can restore to a previous state. What would that artifact look like?
struct UndoableAction {
let text: String
let someToggle: Bool
let scrollPosition: CGFloat
let font: Font
}
In order for our undo model to be initialized with the necessary data the fields in our originator model that hold that data need to be public.
let copyForUndoing = UndoableAction()
copyforUndoing.text = editor.text
copyforUndoing.someToggle = editor.someToggle
copyforUndoing.scrollPosition = editor.scrollPosition
copyforUndoing.font = editor.font
Even if we pass the editor model to the undo's initializer to hide this assignment dance, all its properties still need to be public in order to be copied, exposing all of it's internal state. This highlights the main issue Memento proposes to fix and that is the problem of encapsulation. Should other models come to rely on these public properties, any changes to them will mean changes to other parts of the app that didn't really need to know the editor's implementation details in the first place.
What the Memento pattern proposes is we let each model that needs to have snapshots of itself saved do the snapshot creation instead of delegating it to some external entity.

Let's give this some shape so we can dive into more code.

Here's a simple text editor with an undo button at the top. We're going with the very basic saving of the text that is typed into the text field and then undoing the most recent typing when a user taps the undo button.
Let's look at what our 3 models (memento, originator and caretaker) look like:
/// Originator
struct TextEditorData {
var text: String = ""
}
final class Memento {
let name: String
let snapshotDate: Date
let text: String
init(
name: String,
snapshotDate: Date,
originator: TextEditorData
) {
self.name = name
self.snapshotDate = snapshotDate
self.text = originator.text
}
}
@Observable
final class Caretaker {
var originator = TextEditorData()
var history = [Memento]()
func onTextWillChange() {
// We save snapshots here
}
func undo() {
// We restore snapshot here
}
}
The solution presented here is clunkier but serves the purpose of exhibiting this pattern's logic and could still be useful for undoing other things like changing values across multiple fields on a form.
A crucial detail for Memento to work is that we have to save the data that we want to be able to reverse to before any changes happen. To do that we're going to rely on a combination of the .onChange(of:) view modifier and a debouncing strategy using a Timer. .onChange is going to fire each time the TextEditor's text changes:
TextEditor(text: $caretaker.originator.text)
.border(.gray)
.onChange(of: caretaker.originator.text) { _,_ in
// Cancel any existing timer
caretaker.timer?.invalidate()
// Start a new timer
caretaker.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
caretaker.onTextChanged()
}
}
That behavior is actually a problem since .onChange will fire for each individual character added to the text String. When we're typing–and this might not be obvious the first time–we have the built-in expectation that an editor is smart about undoing. We expect a text editor to undo blocks of changes. If I type "Lorem ipsum dolor sit amet" without stopping, we don't expect undo to result in "Lorem ipsum dolor sit ame", we expect it to empty the editor.
That's why we're debouncing. Any character change creates a new 1 second timer that will only actually fire once the user stops typing.
Also–because of this limitation in how we can observe changes to properties in SwiftUI (we only get them after they happened and not before)–we're going to save the text after it changed, a deviation from what Memento prescribes. That will require us to undo to the second to last Memento in history, like the image below illustrates.

In addition, for this case, we'll have to save the editor's empty state when the user first opens the app so we can reverse to it too.
import Foundation
@Observable
final class Caretaker {
var originator = TextEditorData()
var history = [Memento]()
func onTextChanged() {
saveSnapshot()
}
func undo() {
guard history.count > 1 else { return }
let _ = history.remove(at: 0) // Discard the first because we're saving AFTER a change and not BEFORE
let memento = history.remove(at: 0)
originator.restore(from: memento)
saveSnapshot()
}
func onViewAppeared() {
saveSnapshot()
}
private func saveSnapshot() {
let state = originator.save()
history.insert(state, at: 0)
}
}
import SwiftUI
struct ContentView: View {
@State private var caretaker = Caretaker()
@State private var timer: Timer?
var body: some View {
NavigationStack {
VStack(alignment: .leading, spacing: 8) {
TextEditor(text: $caretaker.originator.text)
.border(.gray)
.onChange(of: caretaker.originator.text) { _,_ in
// Cancel any existing timer
timer?.invalidate()
// Start a new timer
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
caretaker.onTextChanged()
}
}
Text("History:")
.font(.title2)
.bold()
ScrollView {
ForEach(caretaker.history) { item in
HistoryView(memento: item)
}
}
}
.padding()
.navigationTitle("Text Editor")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
caretaker.undo()
} label: {
Image(systemName: "arrow.uturn.backward.circle")
}
.disabled(caretaker.history.count == 1)
}
}
}
.onAppear {
caretaker.onViewAppeared()
}
.onDisappear {
timer?.invalidate()
timer = nil
}
}
}
#Preview {
ContentView()
}
import Foundation
/// Originator
struct TextEditorData {
var text: String = ""
func save() -> Memento {
print("Saving '\(text)'")
return Memento(
name: "Text changed (\(text))",
snapshotDate: .now,
originator: self
)
}
mutating func restore(from memento: Memento) {
self.text = memento.text
}
}
Our code in action... Or is it?
As you can see in the video, this code works but with a catch: changes are getting duplicated, requiring us to undo twice if we really want a previous version. Let's fix it. First we need our Memento class to have a way of being comparable so we can avoid resaving an existing version. For that we're going to reach for Hashable.
import Foundation
final class Memento: Identifiable, Hashable {
let id: UUID
let name: String
let snapshotDate: Date
let text: String
init(
name: String,
snapshotDate: Date,
originator: TextEditorData
) {
self.id = UUID()
self.name = name
self.snapshotDate = snapshotDate
self.text = originator.text
}
static func == (lhs: Memento, rhs: Memento) -> Bool {
lhs.text == rhs.text
}
func hash(into hasher: inout Hasher) {
hasher.combine(text)
}
}
Memento was made Identifiable before so I could show History below the editor.
We could just compare text, which is the only originator property that actually changes in our case, but using Hashable allows us to hash many different properties without leaking them to the caretaker.
Now the saveSnapshot function will change, to avoid resaving a duplicate:
private func saveSnapshot() {
let state = originator.save()
if history.isEmpty {
history.insert(state, at: 0)
return
}
guard
let firstMemento = history.first,
state != firstMemento
else { return }
history.insert(state, at: 0)
}
We're grabbing a snapshot from the editor/originator, checking if it's the first time we save so we can have the first empty state when the user opens the app and then doing a guard so we avoid resaving. Let's run the app again.
The improved version.
Ok, now it's more like it. Our editor takes text and the app is able to undo changes just like we expected.
Wrapping Up
The Memento pattern offers a powerful lens through which to think about state and undo functionality. It gives us a formal way to snapshot and restore without violating encapsulation, which is often a trap when working with shared models or trying to reverse changes from the outside.
In practice, especially on iOS, we have to balance idealized design patterns with the tools the platform gives us. UIKit’s UndoManager is more appropriate for text editing and gets us undo/redo with minimal effort. But for cases where that isn’t available—or for more complex state that spans multiple fields or actions—the Memento pattern still shines.
Ultimately, this example isn’t about building the best undo system for a real app. It’s about exercising our understanding of a classic design pattern, seeing where it fits, where it struggles, and how we might adapt it to modern Swift code.
That’s the value of learning patterns like this: not to follow them blindly, but to expand the way we think about problems and recognize the trade-offs at play.
Be sure to check back soon for Part 2, where I’ll dive into another design pattern and how it applies to real-world iOS development. You can also subscribe below to get new posts delivered straight to your inbox. Thanks for reading — see you next time!