Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

7. Memento Pattern
Written by Joshua Greene

The memento pattern allows an object to be saved and restored. It has three parts:

  1. The originator is the object to be saved or restored.

  2. The memento represents a stored state.

  3. The caretaker requests a save from the originator and receives a memento in response. The caretaker is responsible for persisting the memento and, later on, providing the memento back to the originator to restore the originator’s state.

While not strictly required, iOS apps typically use an Encoder to encode an originator’s state into a memento, and a Decoder to decode a memento back to an originator. This allows encoding and decoding logic to be reused across originators. For example, JSONEncoder and JSONDecoder allow an object to be encoded into and decoded from JSON data respectively.

When should you use it?

Use the memento pattern whenever you want to save and later restore an object’s state.

For example, you can use this pattern to implement a save game system, where the originator is the game state (such as level, health, number of lives, etc), the memento is saved data, and the caretaker is the gaming system.

You can also persist an array of mementos, representing a stack of previous states. You can use this to implement features such as undo/redo stacks in IDEs or graphics software.

Playground example

Open FundamentalDesignPattern.xcworkspace in the Starter directory, or continue from your own playground workspace from the last chapter, and then open the Overview page.

You’ll see Memento is listed under Behavioral Patterns. This is because this pattern is all about save and restoration behavior. Click on the Memento link to open that page.

You’ll create a simple gaming system for this example. First, you need to define the originator. Enter the following right after Code Example:

import Foundation

// MARK: - Originator
public class Game: Codable {

  public class State: Codable {
    public var attemptsRemaining: Int = 3
    public var level: Int = 1
    public var score: Int = 0
  }
  public var state = State()

  public func rackUpMassivePoints() {
    state.score += 9002
  }

  public func monstersEatPlayer() {
    state.attemptsRemaining -= 1
  }
}

Here, you define a Game: it has an internal State that holds onto game properties, and it has methods to handle in-game actions. You also declare Game and State conform to Codable.

What’s Codable? Great question!

Apple introduced Codable in Swift 4. Any type that conforms to Codable can, in Apple’s words, “convert itself into and out of an external representation.” Essentially, it’s a type that can save and restore itself. Sound familiar? Yep, it’s exactly what you want the originator to be able to do.

Since all of the properties that Game and State use already conform to Codable, the compiler automatically generates all required Codable protocol methods for you. String, Int, Double and most other Swift-provided types conform to Codable out of the box. How awesome is that?

More formally, Codable is a typealias that combines the Encodable and Decodable protocols. It’s declared like this:

typealias Codable = Decodable & Encodable

Types that are Encodable can be converted to an external representation by an Encoder. The actual type of the external representation depends on the concrete Encoder you use. Fortunately, Foundation provides several default encoders for you, including JSONEncoder for converting objects to JSON data.

Types that are Decodable can be converted from an external representation by a Decoder. Foundation has you covered for decoders too, including JSONDecoder to convert objects from JSON data.

Great! Now that you’ve got the theory under your belt, you can continue coding.

You next need a memento. Add the following after the previous code:

// MARK: - Memento
typealias GameMemento = Data

Technically, you don’t need to declare this line at all. Rather, it’s here to inform you the GameMemento is actually Data. This will be generated by the Encoder on save, and used by the Decoder on restoration.

Next, you need a caretaker. Add the following after the previous code:

// MARK: - CareTaker
public class GameSystem {

  // 1
  private let decoder = JSONDecoder()
  private let encoder = JSONEncoder()
  private let userDefaults = UserDefaults.standard

  // 2
  public func save(_ game: Game, title: String) throws {
    let data = try encoder.encode(game)
    userDefaults.set(data, forKey: title)
  }

  // 3
  public func load(title: String) throws -> Game {
    guard let data = userDefaults.data(forKey: title),
      let game = try? decoder.decode(Game.self, from: data)
      else {
      throw Error.gameNotFound
    }
    return game
  }

  public enum Error: String, Swift.Error {
    case gameNotFound
  }
}

Here’s what this does:

  1. You first declare properties for decoder, encoder and userDefaults. You’ll use decoder to decode Games from Data, encoder to encode Games to Data, and userDefaults to persist Data to disk. Even if the app is re-launched, saved Game data will still be available.

  2. save(_:title:) encapsulates the save logic. You first use encoder to encode the passed-in game. This operation may throw an error, so you must prefix it with try. You then save the resulting data under the given title within userDefaults.

  3. load(title:) likewise encapsulates the load logic. You first get data from userDefaults for the given title. You then use decoder to decode the Game from the data. If either operation fails, you throw a custom error for Error.gameNotFound. If both operations succeed, you return the resulting game.

You’re ready for the fun part: using the classes!

Add the following to the end of the playground page:

// MARK: - Example
var game = Game()
game.monstersEatPlayer()
game.rackUpMassivePoints()

Here you simulate playing a game: the player gets eaten by a monster, but she makes a comeback and racks up massive points!

Next, add the following code to the end of the playground page:

// Save Game
let gameSystem = GameSystem()
try gameSystem.save(game, title: "Best Game Ever")

Here, you simulate the player triumphantly saving her game, likely boasting to her friends shortly thereafter.

Of course, she will want to try to beat her own record, so she’ll start a new Game. Add the following code to the end of the playground page:

// New Game
game = Game()
print("New Game Score: \(game.state.score)")

Here, you create a new Game instance and print out the game.state.score. This should print the following to the console:

New Game Score: 0

This proves the default value is set for game.state.score.

The player can also resume her previous game. Add the following code to the end of the playground page:

// Load Game
game = try! gameSystem.load(title: "Best Game Ever")
print("Loaded Game Score: \(game.state.score)")

Here, you load the player’s previous Game, and print the game’s score. You should see this in your output:

Loaded Game Score: 9002

Keep on winning, player!

What should you be careful about?

Be careful when adding or removing Codable properties: both encoding and decoding can throw an error. If you force unwrap these calls using try! and you’re missing any required data, your app will crash!

To mitigate this problem, avoid using try! unless you’re absolutely sure the operation will succeed. You should also plan ahead when changing your models.

For example, you can version your models or use a versioned database. However, you’ll need to carefully consider how to handle version upgrades. You might choose to delete old data whenever you encounter a new version, create an upgrade path to convert from old to new data, or even use a combination of these approaches.

Tutorial project

You’ll continue the RabbleWabble app from the previous chapter.

If you skipped the previous chapter, or you want a fresh start, open Finder and navigate to where you downloaded the resources for this chapter. Then, open starter ▸ RabbleWabble ▸ RabbleWabble.xcodeproj in Xcode.

You’ll use the memento pattern to add an important app feature: the ability to save QuestionGroup scores.

Open QuestionGroup.swift, and add the following right after the opening class curly brace:

public class Score: Codable {
  public var correctCount: Int = 0
  public var incorrectCount: Int = 0
  public init() { }
}

Here you a create a new class called Score, which you’ll use to hold on to score info.

Then, add the following property right after questions, ignoring the compiler errors for now:

public var score: Score

To fix the compiler errors, you need to declare a new initializer. Add the following right before the ending class curly brace:

public init(questions: [Question],
            score: Score = Score(),
            title: String) {
  self.questions = questions
  self.score = score
  self.title = title
}

This initializer has a default value for the score property, creating a blank Score object. That means everywhere in the app that was creating a QuestionGroup before using init(questions:title:) can still do so and they will get this blank Score object created for them.

Lastly, replace public struct QuestionGroup with the following, again, ignoring the resulting compiler error for now:

public class QuestionGroup: Codable

QuestionGroup will act as the originator. You change this from a struct to a class to make this to a reference type instead of a value type, so you can pass around and modify QuestionGroup objects instead of copying them. You also make it conform to Codable to enable encoding and decoding.

Since Question doesn’t currently conform to Codable, the compiler can’t generate the required protocol methods automatically for you. Fortunately, this is easy to fix.

Open Question.swift and replace public struct Question with this:

public class Question: Codable

You change Question from a struct to a class to make this a reference type, and you also make it conform to Codable.

You also need to add an initializer for this class. Add the following before the closing class curly brace:

public init(answer: String, hint: String?, prompt: String) {
  self.answer = answer
  self.hint = hint
  self.prompt = prompt
}

Build your project to verify you’ve resolved all of the compiler errors.

Next, right-click on the yellow RabbleWabble group, select New Group and name it Caretakers.

Right-click again on the yellow RabbleWabble group and select Sort by Name.

Your File hierarchy should now look like this:

Right-click on your newly-added Caretakers group and select New File. Under the iOS tab, select Swift File and click Next. Enter DiskCaretaker.swift for the name and click Create.

Replace the contents of DiskCaretaker.swift with the following:

import Foundation

public final class DiskCaretaker {
  public static let decoder = JSONDecoder()
  public static let encoder = JSONEncoder()
}

DiskCaretaker will ultimately provide methods for saving and retrieving Codable objects from the device’s Documents directory. You’ll use JSONEncoder to encode objects into JSON data and JSONDecoder to decode from JSON data into objects.

Add the next block of code before the closing class curly brace:

public static func createDocumentURL(
  withFileName fileName: String) -> URL {
  let fileManager = FileManager.default
  let url = fileManager.urls(for: .documentDirectory,
                             in: .userDomainMask).first!
  return url.appendingPathComponent(fileName)
    .appendingPathExtension("json")
}

You’ll use this method to create a document URL given a fileName. This method simply finds the Documents directory and then appends the given file name.

Add this method right before createDocumentURL(withFileName:):

// 1
public static func save<T: Codable>(
  _ object: T, to fileName: String) throws {
  do {
    // 2
    let url = createDocumentURL(withFileName: fileName)
    // 3
    let data = try encoder.encode(object)
    // 4
    try data.write(to: url, options: .atomic)
  } catch (let error) {
      // 5
      print("Save failed: Object: `\(object)`, " +
        "Error: `\(error)`")
      throw error
  }
}

You’ll use this method to save Codable objects.

Here’s how it works, line-by-line:

  1. You first declare a generic method that takes any object that conforms to Codable.

  2. You then call createDocumentURL to create a document URL for the given fileName.

  3. You use encoder to encode the object into data. This operation may throw an error, so you prefix it with try.

  4. You call data.write to write the data to the given url. You use the atomic operator to instruct iOS to create a temporary file and then move it to the desired path. This has a small performance cost, but it ensures the file data will never be corrupted. It’s possible this operation may throw an error, so you must prefix it with try.

  5. If you catch an error, you print the object and error to the console and then throw the error.

Next, add these methods right after save(_:to:):

// 1
public static func retrieve<T: Codable>(
  _ type: T.Type, from fileName: String) throws -> T {
  let url = createDocumentURL(withFileName: fileName)
  return try retrieve(T.self, from: url)
}

// 2
public static func retrieve<T: Codable>(
  _ type: T.Type, from url: URL) throws -> T {
  do {
    // 3
    let data = try Data(contentsOf: url)
    // 4
    return try decoder.decode(T.self, from: data)
  } catch (let error) {
    // 5
    print("Retrieve failed: URL: `\(url)`, Error: `\(error)`")
    throw error
  }
}

Here’s what’s going on:

  1. You declare a method for retrieving objects given a type and fileName, which is a String. This method first creates a file URL and calls retrieve(_:from:). You’ll soon see how it can be useful to pass either a String or URL at times to retrieve persisted objects.

  2. You also declare a method which takes a URL rather than a String, which does the actual loading. The previous method simply calls through to this one. You’ll need both, so both are public.

  3. Here you attempt to create a Data instance from the given file url. It’s possible this operation may fail, so you prefix this call with try.

  4. You then use decoder to decode the object into data. This operation may throw an error, so you prefix it with try.

  5. If you catch an error, you print the url and error to the console and then throw the error.

Great start! You’ll soon see how useful this helper class is. However, you need to create another file first.

Right-click on the Caretakers group and select New File. Under the iOS tab, select Swift File and click Next. Enter QuestionGroupCaretaker.swift for the name and click Create.

Replace the contents of QuestionGroupCaretaker.swift with the following:

import Foundation

// 1
public final class QuestionGroupCaretaker {

  // MARK: - Properties
  // 2
  private let fileName = "QuestionGroupData"
  public var questionGroups: [QuestionGroup] = []
  public var selectedQuestionGroup: QuestionGroup!

  // MARK: - Object Lifecycle
  public init() {
    // 3
    loadQuestionGroups()
  }

  // 4
  private func loadQuestionGroups() {
    if let questionGroups =
      try? DiskCaretaker.retrieve([QuestionGroup].self,
                                  from: fileName) {
      self.questionGroups = questionGroups
    } else {
      let bundle = Bundle.main
      let url = bundle.url(forResource: fileName,
                           withExtension: "json")!
      self.questionGroups = try!
        DiskCaretaker.retrieve([QuestionGroup].self, from: url)
      try! save()
    }
  }

  // MARK: - Instance Methods
  // 5
  public func save() throws {
    try DiskCaretaker.save(questionGroups, to: fileName)
  }
}

Here’s what this does:

  1. You declare a new class called QuestionGroupCaretaker. You’ll use this to save and retrieve QuestionGroup objects.

  2. You declare three properties: fileName defines the file where you’ll save and retrieve QuestionGroup objects; questionGroups will hold onto the QuestionGroups that are in use; and selectedQuestionGroup will hold onto whichever selection the user makes.

  3. You call loadQuestionGroups() inside init(), which loads the question groups.

  4. You perform the retrieve actions within loadQuestionGroups(). First, you attempt to load QuestionGroups from the user’s Documents directory using fileName. If the file hasn’t been created, such as the first time the app is launched, this will fail and return nil instead.

    In the case of a failure, you load the QuestionGroups from Bundle.main and then call save() to write this file to the user’s Documents directory.

    However, you haven’t added QuestionGroupsData.json to the main bundle yet. You’ll need to do this next.

Open Finder and navigate to where you have the projects downloaded for this chapter. Alongside the Starter and Final directories, you’ll see a Resources directory that contains QuestionGroupData.json.

Position the Finder window above Xcode and drag and drop QuestionGroupData.json into the Resources group like so:

In the new window that appears, make sure Copy items if needed is checked and click Finish to copy the file.

Next, you actually need to use QuestionGroupCaretaker.

Open SelectQuestionGroupViewController and replace the let questionGroups line with the following:

private let questionGroupCaretaker = QuestionGroupCaretaker()
private var questionGroups: [QuestionGroup] {
  return questionGroupCaretaker.questionGroups
}

Replace the var selectedQuestionGroup line with the following:

private var selectedQuestionGroup: QuestionGroup! {
  get { return questionGroupCaretaker.selectedQuestionGroup }
  set { questionGroupCaretaker.selectedQuestionGroup = newValue }
}

Since you’re no longer using QuestionGroupData.swift, select this file within the File navigator and click Delete. In the new window that appears, select Move to Trash.

Build and run, and verify everything works as before.

The very first time you run the app, you’ll see an error printed containing this text:

The file "QuestionGroupData.json" couldn’t be opened because there is no such file.

This is because loadQuestionGroups() in QuestionGroupCaretaker tries to read QuestionGroupData.json from the Documents directory, but this file won’t exist when the app is first launched. However, the app handles this gracefully; it reads QuestionGroupData.json from the main bundle and saves it to the Documents directory for future reads.

Build and run again, and you shouldn’t see any errors logged to the console. Everything works so far. However, what about saving the QuestionGroup’s score?

Open SequentialQuestionStrategy.swift and replace these two lines:

public var correctCount: Int = 0
public var incorrectCount: Int = 0

With this:

public var correctCount: Int {
  get { return questionGroup.score.correctCount }
  set { questionGroup.score.correctCount = newValue }
}
public var incorrectCount: Int {
  get { return questionGroup.score.incorrectCount }
  set { questionGroup.score.incorrectCount = newValue }
}

Rather than using stored properties for correctCount and incorrectCount, you get and set the questionGroup.score.correctCount and questionGroup.score.incorrectCount respectively.

But wait, isn’t there similar logic in RandomQuestionStrategy.swift too? Yes, there is! While you could try to copy this logic over as well, you’d end up duplicating a lot of code.

This brings up an important point: when you add new design patterns and functionality to your app, you’ll need to refactor your code occasionally. In this case, you’ll pull out a base class to move your shared logic into.

Right-click on the Strategies group and select New file…. Under the iOS tab, select Swift File and click Next. Enter BaseQuestionStrategy.swift for the name and click Create.

Replace the contents of BaseQuestionStrategy.swift with the following:

public class BaseQuestionStrategy: QuestionStrategy {

  // MARK: - Properties
  // 1
  public var correctCount: Int {
    get { return questionGroup.score.correctCount }
    set { questionGroup.score.correctCount = newValue }
  }
  public var incorrectCount: Int {
    get { return questionGroup.score.incorrectCount }
    set { questionGroup.score.incorrectCount = newValue }
  }
  private var questionGroupCaretaker: QuestionGroupCaretaker

  // 2
  private var questionGroup: QuestionGroup {
    return questionGroupCaretaker.selectedQuestionGroup
  }
  private var questionIndex = 0
  private let questions: [Question]

  // MARK: - Object Lifecycle
  // 3
  public init(questionGroupCaretaker: QuestionGroupCaretaker,
              questions: [Question]) {
    self.questionGroupCaretaker = questionGroupCaretaker
    self.questions = questions

    // 4
    self.questionGroupCaretaker.selectedQuestionGroup.score =
      QuestionGroup.Score()
  }

  // MARK: - QuestionStrategy
  public var title: String {
    return questionGroup.title
  }

  public func currentQuestion() -> Question {
    return questions[questionIndex]
  }

  public func advanceToNextQuestion() -> Bool {
    guard questionIndex + 1 < questions.count else {
      return false
    }
    questionIndex += 1
    return true
  }

  public func markQuestionCorrect(_ question: Question) {
    correctCount += 1
  }

  public func markQuestionIncorrect(_ question: Question) {
    incorrectCount += 1
  }

  public func questionIndexTitle() -> String {
    return "\(questionIndex + 1)/\(questions.count)"
  }
}

If you compare this to RandomQuestionStrategy, you’ll find this is very similar. However, there are a few important differences:

  1. You use the underlying questionGroup.score.correctCount and questionGroup.score.incorrectCount instead of stored properties.

  2. The questionGroup is actually a computed property, which returns questionGroupCaretaker.selectedQuestionGroup.

  3. Here, you’ve added a new initializer to accept a QuestionGroupCaretaker and Questions instead of a QuestionGroup. You’ll use questionGroupCaretaker to persist changes to disk, and questions will be an ordered array for displaying the Question.

  4. Here, you reset the score to a new instance, Score(), so scoring always starts over whenever you start a QuestionGroup.

The rest of the code is pretty much what already existed in RandomQuestionStrategy and SequentialQuestionStrategy.

You next need to refactor RandomQuestionStrategy to subclass BaseQuestionStrategy.

Open RandomQuestionStrategy.swift and replace its contents with the following, ignoring the resulting compiler errors for now:

import GameplayKit.GKRandomSource

public class RandomQuestionStrategy: BaseQuestionStrategy {

  public convenience init(
    questionGroupCaretaker: QuestionGroupCaretaker) {
    let questionGroup =
      questionGroupCaretaker.selectedQuestionGroup!
    let randomSource = GKRandomSource.sharedRandom()
    let questions = randomSource.arrayByShufflingObjects(
      in: questionGroup.questions) as! [Question]
    self.init(questionGroupCaretaker: questionGroupCaretaker,
              questions: questions)
  }
}

This code is much shorter than before, isn’t it? This is because most of the logic is handled within BaseQuestionStrategy.

RandomQuestionStrategy simply shuffles the questions in a random order and passes the resulting questions array to init(questionGroupCaretaker:questions:), which is the initializer on the base class.

Next, open SequentialQuestionStrategy.swift and replace its contents with the following; again, ignore any compiler errors in other files for now:

public class SequentialQuestionStrategy: BaseQuestionStrategy {

  public convenience init(
    questionGroupCaretaker: QuestionGroupCaretaker) {
    let questionGroup =
      questionGroupCaretaker.selectedQuestionGroup!
    let questions = questionGroup.questions
    self.init(questionGroupCaretaker: questionGroupCaretaker,
              questions: questions)
  }
}

SequentialQuestionStrategy simply passes questions in the same order as they are defined on questionGroupCaretaker.selectedQuestionGroup! to init(questionGroupCaretaker:questions:).

Next, you need to fix the compiler errors caused by these changes.

Open AppSettings.swift and replace questionStrategy(for:) inside of QuestionStrategyType with the following, ignoring any resulting compiler errors:

public func questionStrategy(
  for questionGroupCaretaker: QuestionGroupCaretaker)
  -> QuestionStrategy {
    switch self {
    case .random:
      return RandomQuestionStrategy(
        questionGroupCaretaker: questionGroupCaretaker)
    case .sequential:
      return SequentialQuestionStrategy(
        questionGroupCaretaker: questionGroupCaretaker)
    }
}

You change this to accept a QuestionGroupCaretaker instead of a QuestionGroup, so you can use the convenience initializers you just created on RandomQuestionStrategy and SequentialQuestionStrategy.

Next, replace questionStrategy(for:) inside of AppSettings with the following; again, ignore the resulting compiler errors for now:

public func questionStrategy(
  for questionGroupCaretaker: QuestionGroupCaretaker)
  -> QuestionStrategy {
  return questionStrategyType.questionStrategy(
    for: questionGroupCaretaker)
}

Likewise, you update this method to take a QuestionGroupCaretaker instead of a QuestionGroup.

There’s one more compiler error you need to fix. Open SelectQuestionGroupViewController.swift and replace this line:

viewController.questionStrategy =
  appSettings.questionStrategy(for: selectedQuestionGroup)

with this:

viewController.questionStrategy =
  appSettings.questionStrategy(for: questionGroupCaretaker)

Build and run and select a QuestionGroup cell to verify everything works.

Awesome! You’re finally ready to save the scores from QuestionGroups.

Open BaseQuestionStrategy.swift and add the following to advanceToNextQuestion(), right after this method’s opening curly brace:

try? questionGroupCaretaker.save()

This performs a save whenever the next question is requested.

To verify this works, open SelectQuestionGroupViewController.swift and add the following code at the end of the main SelectQuestionGroupViewController class definition:

// MARK: - View Lifecycle
public override func viewDidLoad() {
  super.viewDidLoad()
  questionGroups.forEach {
    print("\($0.title): " +
      "correctCount \($0.score.correctCount), " +
      "incorrectCount \($0.score.incorrectCount)"
    )
  }
}

Here, you print the title, score.correctCount and score.incorrectCount for each QuestionGroup.

Build and run; select any QuestionGroup cell you’d like; and tap the green checkmark and red X buttons a few times to mark the questions as correct and incorrect. Then, stop the app and build and run again. You should see output like this in the console:

Hiragana: correctCount 22, incorrectCount 8
Katakana: correctCount 0, incorrectCount 0
Basic Phrases: correctCount 0, incorrectCount 0
Numbers: correctCount 0, incorrectCount 0

Excellent! This shows the scores are saved across app launches.

Key points

You learned about the memento pattern in this chapter. Here are its key points:

  • The memento pattern allows an object to be saved and restored. It involves three types: the originator, memento and caretaker.

  • The originator is the object to be saved; the memento is a saved state; and the caretaker handles, persists and retrieves mementos.

  • iOS provides Encoder for encoding a memento to, and Decoder for decoding from, a memento. This allows encoding and decoding logic to be used across originators.

Rabble Wabble is really coming along, and you can now save and restore scores! However, the app doesn’t show the score to the user yet. You’ll use another pattern to do this: The observer pattern.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.