Memento Pattern with SwiftUI

tutorial memento in swift swiftui

Hallo allemaal, Leo hier. I’m glad to announce that we will study one of the classic twenty-three design patterns, we will create an implementation of the Memento pattern in SwiftUI. We will mix a very old technique with the new UI framework in this tutorial article.

The main motivation to start studying this was a colleague of mine that is studying design patterns, and she knows that I like those talking to me about Memento and how that is something cool. Later on, I started my research about the Memento Pattern in iOS and it is really useful. The examples of use are vast and one could argue that in a lot of places where you have a user interaction that save a state you could use this pattern to make your life easier.

Design patterns are useful because they create are easy to understand for anyone who is reading that code. For example, imagine someone that doesn’t know Swift but this person starts to read a project that has a CartMemento , the understanding of the coding will be immediate if this person knows the Memento design pattern. This way design patterns are good to create a shared understanding of what and how things are being solved in the code.

It is always good to remember that design patterns are exceptional to solve the things they were built to solve. Do not try to put layers and layers of design patterns in your code because you can, you could end up doing exactly the opposite, an anti-pattern. You could end up with spaghetti code and that is not desirable for anyone.

No more talking let’s code! But first…

 

Painting of The Day

The painting today is called Memento Mori, anVII century art piece by Frans van Everbroeck.

Frans van Everbroeck (c. 1628– between 1676 and 1693) was a Flemish still life painter who is known for his fruit still lifes, vanitas still lives, and pronkstillevens. He was active in Antwerp, Amsterdam, and London. The Dutch painters Abraham Mignon and Maria van Oosterwyck are regarded as their followers.

Van Everbroeck is also known for his vanitas still lifes, a genre of still lifes which offers a reflection on the meaninglessness of earthly life and the transient nature of all earthly goods and pursuits. An example is the composition Memento Mori (At Van Ham Kunstauktionen on 17 May 2013 in Cologne, lot 478). This composition contains the typical symbolism of vanitas paintings: a skull, soap bubbles, a candle, an hourglass, a watch, and a book (symbolizing the futility of mankind’s higher aspirations).

I chose this painting because we will talk about the mento pattern today, and to remember the brevity of things.

 

The Problem – Memento Pattern in SwiftUI

Imagine that you have a shopping cart in your app and you want to give the opportunity to the users to UNDO their actions. For example, if a user deletes something he can undo the deleting, or if he adds something to the shopping cart he could undo that too.

Before diving into the implementation, let’s talk a little bit about the memento pattern.

The Memento design pattern is one of the 23 well-known GoF design patterns that describe how to solve recurring design problems to design flexible and reusable object-oriented software, that is, objects that are easier to implement, change, test, and reuse. The Memento Pattern was created by Noah Thompson, David Espiritu, and Dr. Drew Clinkenbeard for early HP products.

It is a behavioral design pattern which means they are design patterns that identify how objects communicate with each other. By doing so, these patterns increase flexibility in carrying out communication.

We will create a simple app that you can add and remove items from a list, and also undo those operations. Like the example below:

 

What problems does the Memento design in Swift solve?

The Memento design pattern will solve mainly two problems. Check them below:

  1. The state of an object should be stored externally, and this should be reversible, in other words, you should be able to restore it later if you want.
  2. The object encapsulation should be preserved.

 

The first thing is common ground in iOS development right? A lot of apps could have benefited from this feature. Any app that has a list and that lets a user erase things from that list is a potential use case for this pattern.

Suppose you have a weather app that users can add their favorite locations to it and of course delete them. When the user deletes the location you could undo the delete operation giving another opportunity for any missed interaction to be easily solved. Or even a calculator app that users can input their equations and you could use this to snapshot to revert any number or operation done.

Another example is, imagine that you have a flight tracker app that users can upload flight documents to it, if a user accidentally removes the document you could implement a memento to easily undo that operation.

The application of this pattern in iOS development is incredible.

But how is the memento solution?

 

Memento in Swift Described

The memento pattern is really simple. Only involves three objects, the Caretaker, the Originator, and the Memento Object. And let’s give an example of a shopping cart, which would work something like this:

  1. The caretaker uses the originator as a source of truth for the cart state.
  2. When the caretaker needs, it asks the originator to create a snapshot (memento) of the actual state of the shopping cart. In our example, every time a user adds or deletes, we will generate a new snapshot(memento) and save it in a stack.
  3. The caretaker holds these snapshots(mementos) and can restore them whenever they want using the originator to do that.

 

Simple but mighty. This setup gives the caretaker freedom to only know the things it should know about the state and leaves all the heavy lifting for the originator. You can find the architectural visual representation of this pattern below:

how to use memento pattern in swift architecture

 

The only one that knows how to transform a memento into a state is the Originator and this preserves the encapsulation of the State and gives the flexibility to use anywhere else in the code with the same capabilities of creating and restoring snapshots of an object.

Observe that having the capability to create a snapshot and restore its state is not a memento pattern. It could be a variation of it but it is not the pattern itself. To be a memento pattern you need to have two things happening at the same time: you can create and restore snapshots, and those snapshots preserve the internal that of the snapshotted object/model.

 

Roles involved in Memento Pattern

Each one of the memento pattern objects has its responsibilities.

The caretaker is responsible to take care of the created snapshots and any other management of that. You could save those snapshots in the user defaults or core that for example. Also, the caretaker owns the originator object but the originator doesn’t know about the caretaker, which makes things easier to test and avoid cyclical references.

The originator has three roles here. Keep the state of the object or representation that could be snapshotted, create a memento of that state and restore a memento of that state. originator is the heart and engine of this pattern. I would say that the caretaker is just someone using the originator’s capabilities, storing its snapshots.

The last one is the Memento Object this object is what the caretaker will store and what the originator produces. It encapsulates everything that the originator needs to restore the state of the, for example, shopping cart.

 

Memento Pattern in Swift Code Example

For the sake of simplicity, we will use SwiftUI for this guide. One thing nice to mention is that I’m using Xcode 14 beta if you are using a lower version you will have to modify the code below to suit your Swift version.

Now start a new SwiftUI project and let’s make our first part of the pattern, the Memento object.

When you are thinking of using the Memento pattern the first thing you have to think is:

What I don’t want to disclose about the state?

That thing is what encapsulation will protect you.

In our shopping cart example, I want to hide that every time a user adds or removes a shopping item, we save the last modified date in the originator state. And hiding that information from the caretaker is exactly what makes this pattern amazing because you could add a lot of other state information, that would be saved by the caretaker but you would never need to change the caretaker because it is not responsible for dealing with creation or restoration of the Memento object. Cool, right?

We’ll start to build from the most simple Memento Object to the view. This way you see all the pieces of this pattern working together as gears in a clock.

 

Creating Memento Object

To create the Memento object we will use three structs, and copy and paste them into your Xcode:

protocol ShoppingCartMemento {}

struct ShoppingCart: ShoppingCartMemento {
    var cartItems: [CartItem]
    let lastModified: Date
}

struct CartItem: Identifiable {
    let id: UUID = UUID()
    let name: String
    let date: Date
}

What the caretaker will store is the ShoppingCartMemento , this way encapsulates the lastModified and cartItems parameters from anyone that would be the caretaker for that class.

 

Creating the Originator

It’s time to build the engine of this pattern. The ShoppingCartOriginator will be responsible for maintaining the state, in our case, this includes also adding and removing operations, and also responsible for creating and restoring mementos.

Let’s check the code for that, copy and paste the code below into your project:

class ShoppingCartOriginator {
    
    @Published private(set) var cartState = [CartItem]()
    private var lastModified = Date() // Mark 1
    
    func removeCartItem() { // Mark 2
        guard !cartState.isEmpty else {
            return
        }
        
        cartState.removeLast()
    }
    
    func resetState() { // Mark 2
        cartState = []
        lastModified = Date()
    }
    
    func addCartItem(itemName: String) { // Mark 2
        let newCartItem = CartItem(name: itemName, date: Date())
        lastModified = Date()
        cartState.append(newCartItem)
    }
    
    func createMemento() -> ShoppingCartMemento { // Mark 3 - Memento Pattern requirement
        return ShoppingCart(cartItems: cartState, lastModified: lastModified)
    }
    
    func restoreMemento(shoppingCartMemento: any ShoppingCartMemento) { // Mark 3 - Memento Pattern requirement
        guard let shoppingCart = shoppingCartMemento as? ShoppingCart else { return } // in your app this could be operation like search for an ID in an API
        lastModified = shoppingCart.lastModified
        cartState = shoppingCart.cartItems
    }
} 

Explaining each mark:

  1. Here is the property we want to hide from anyone outside. It is part of the cart state but is not a cartItem. Whatever we want to recreate one particular cart state, we should also be able to have the exact date from that cart.
  2. Mark 2 is just to make it easier to change things. We could only change the state of the ShoppingCartOriginator using the functions that the originator provide to us. This is great for encapsulation and code maintainability.
  3. Mark 3 is where we start to touch the Memento pattern. Those two methods are what we are discussing all this article, a way to create the memento object and a way to restore it. In your application, the cart memento could be way more complex than our case, we are just using an optional downcast here. For example, your memento object could have an ID and you could search for that in your core data or web API.

The other thing here is the published cart state to easily integrate with other parts of the app.

 

Creating the Caretaker

Our caretaker will be our CartService. He will be responsible to say to the originator when to save the state and also keep the state. The caretaker is also responsible for calling the restoring memento object on the originator.

Copy and paste the code below:

class CartService: ObservableObject {
    
    @Published private(set) var cartItems = [CartItem]() // Mark 1
    private var cartMementoStack = [any ShoppingCartMemento]()
    private let cartItemOriginator = ShoppingCartOriginator()
    private var cancellable: AnyCancellable?
    
    init() { // Mark 2
        cancellable = cartItemOriginator
            .$cartState
            .assign(to: \.cartItems, on: self)
    }
    
    var groceryItem: String {
        ["banana","apple","carrot","ketchup", "juice", "rice"].randomElement() ?? ""
    }
    
    func addCartItem() { // Mark 3
        cartItemOriginator.addCartItem(itemName: groceryItem)
        
        let cartMemento = cartItemOriginator.createMemento()
        
        cartMementoStack.append(cartMemento)
    }
    
    func removeRandomCartItem() { // Mark 4
        cartItemOriginator.removeCartItem()
        
        let cartMemento = cartItemOriginator.createMemento()
        
        cartMementoStack.append(cartMemento)
    }
    
    func undo() { // Mark 5
        guard !cartMementoStack.isEmpty else {
            return
        }
        
        cartMementoStack.removeLast()
        
        guard let lastCartItemList = cartMementoStack.last else {
            cartItemOriginator.resetState()
            return
        }
        
        cartItemOriginator.restoreMemento(shoppingCartMemento: lastCartItemList)
    }
}

In the CartService you have:

  1. Mark 1 you can notice the originator being held by the caretaker. We also declared the cartItem publisher for the sake of simplicity for the view.
  2. Mark 2 is just connecting the publisher from the originator to the publisher of the caretaker.
  3. On Mark 3 we start to build up our features. Here we are doing three things: first adding a new random item for the cart, then we create a memento object of that cart, and finally storing that in our stack.
  4. The opposite occurs in Mark 4 where we remove an item from the stack, then create a snapshot(memento object) of it and then save it into your stack.
  5. This is the undo function. Here we are removing the last item from the mementoStack and peaking the stack using that to restore the state. When we restore the previous state all the publishers work in our favor to update the view.

Now let’s finish creating the view.

 

Creating a SwiftUI to thst the Memento Pattern

Finally lelet’sest our memento with a basic SwiftUI list. Copy/paste the code below in your ContentView.swift:

struct ContentView: View {
    @ObservedObject var cartService = CartService()
    
    var body: some View {
        VStack {
            List() {
                ForEach(cartService.cartItems, id: \.id) { cartItem in
                    Text("\(cartItem.name)")
                }
            }
            
            HStack {
                Button {
                    cartService.undo()
                } label: {
                    Text("Undo")
                }.buttonStyle(.bordered)
                
                Button {
                    cartService.removeRandomCartItem()
                } label: {
                    Text("Remove Item")
                }.buttonStyle(.borderless)
                
                Button {
                    cartService.addCartItem()
                } label: {
                    Text("Add Item")
                }.buttonStyle(.borderedProminent)
            }
        }
    }
}

The result should be this:

I’ll leave also the complete SwiftUI memento code example if you just want to copy/paste it from one place.

 

Complete Swift Memento Code Example

Here I’ll leave the complete example if you are not a fan of the following explanations, you could just copy/paste the code below and check for yourself:

import SwiftUI
import Combine

struct ContentView: View {
    @ObservedObject var cartService = CartService()
    
    var body: some View {
        VStack {
            List() {
                ForEach(cartService.cartItems, id: \.id) { cartItem in
                    Text("\(cartItem.name)")
                }
            }
            
            HStack {
                Button {
                    cartService.undo()
                } label: {
                    Text("Undo")
                }.buttonStyle(.bordered)
                
                Button {
                    cartService.removeRandomCartItem()
                } label: {
                    Text("Remove Item")
                }.buttonStyle(.borderless)
                
                Button {
                    cartService.addCartItem()
                } label: {
                    Text("Add Item")
                }.buttonStyle(.borderedProminent)
                
            }
        }
    }
}

class ShoppingCartOriginator {
    
    @Published private(set) var cartState = [CartItem]()
    private var lastModified = Date()
    
    func removeCartItem() {
        guard !cartState.isEmpty else {
            return
        }
        
        cartState.removeLast()
    }
    
    func resetState() {
        cartState = []
        lastModified = Date()
    }
    
    func addCartItem(itemName: String) {
        let newCartItem = CartItem(name: itemName, date: Date())
        lastModified = Date()
        cartState.append(newCartItem)
    }
    
    func createMemento() -> ShoppingCartMemento {
        return ShoppingCart(cartItems: cartState, lastModified: lastModified)
    }
    
    func restoreMemento(shoppingCartMemento: any ShoppingCartMemento) {
        guard let shoppingCart = shoppingCartMemento as? ShoppingCart else { return }
        lastModified = shoppingCart.lastModified
        cartState = shoppingCart.cartItems
    }
}

class CartService: ObservableObject {
    
    @Published private(set) var cartItems = [CartItem]()
    private var cartMementoStack = [any ShoppingCartMemento]()
    private let cartItemOriginator = ShoppingCartOriginator()
    private var cancellable: AnyCancellable?
    
    init() {
        cancellable = cartItemOriginator
            .$cartState
            .assign(to: \.cartItems, on: self)
    }
    
    var groceryItem: String {
        ["banana","apple","carrot","ketchup", "juice", "rice"].randomElement() ?? ""
    }
    
    func addCartItem() {
        cartItemOriginator.addCartItem(itemName: groceryItem)
        
        let cartMemento = cartItemOriginator.createMemento()
        
        cartMementoStack.append(cartMemento)
    }
    
    func removeRandomCartItem() {
        cartItemOriginator.removeCartItem()
        
        let cartMemento = cartItemOriginator.createMemento()
        
        cartMementoStack.append(cartMemento)
    }
    
    func undo() {
        guard !cartMementoStack.isEmpty else {
            return
        }
        
        cartMementoStack.removeLast()
        
        guard let lastCartItemList = cartMementoStack.last else {
            cartItemOriginator.resetState()
            return
        }
        
        cartItemOriginator.restoreMemento(shoppingCartMemento: lastCartItemList)
    }
}

protocol ShoppingCartMemento {}

struct ShoppingCart: ShoppingCartMemento {
    var cartItems: [CartItem]
    let lastModified: Date
}

struct CartItem: Identifiable {
    let id: UUID = UUID()
    let name: String
    let date: Date
}

And that’s all folks.

 

Continue Studying

If you enjoyed this article and want to continue learning something new about design patterns, I would recommend reading about this the decorator pattern applied to Swift apps. In that article, we explore how can you use the decorator pattern to add features to your flows without having to change any of the objects involved.

Or you could check how Uber deals with dependency injection in their app, following my tutorial about Needle framework. There I explain step by step how to use code generation to create compile-time safe dependency injection the same way Uber does in their iOS app. Cool right?

 

Summary – Memento Pattern with SwiftUI

This week we learned about the memento pattern and implemented that in Swift. We studied the motivations for using this pattern and the moments you shouldn’t use Memento. Also learned why is it called a behavioral pattern and what are the prerequisites to use it.

We also analyzed all the parts that make the memento pattern: the memento object, the caretaker, and the originator. I would suggest you try it yourself and create even a variation for your own needs!

That’s all my people, today we finished the Architecture and iOS Tooling article closing the series about the beginning of iOS development. I hope you liked reading this article as much as I enjoyed writing it. If you want to support this blog you can Buy Me a Coffee or if you are a company you can sponsor this blog. You can reach me on LinkedIn or Twitter and send me an e-mail through the contact page.

Thanks for reading and… That’s all folks.

Credits:

title image

Share this post:

Related posts

Sponsor