UndoManager Tutorial: How to Implement With Swift Value Types

In this tutorial you’ll learn how to build an undo manager, using Swift and value types, leveraging the Foundation’s UndoManager class By Lyndsey Scott.

Leave a rating/review
Download materials
Save for later
Share
Note: This tutorial was built for Xcode 10 and iOS 12.

Nobody’s perfect. And once you implement UndoManager, your users don’t have to be either.

UndoManager provides a simple way to add undo/redo functionality to your apps. You may also be able to prevent the occasional flaw in your reasoning by keeping things more “local”.

In this tutorial, you’ll build an app called People Keeper to improve your local reasoning with Swift value types and learn how to use that improved local reasoning to achieve flawless undo/redo implementations.

Note: This tutorial assumes that you have an intermediate knowledge of iOS and Swift development. If you’re new to iOS development and/or Swift, check out our “Learn to Code iOS Apps with Swift Tutorial” series first.

Getting Started

Download the materials for this tutorial using the Download Materials link found at the top and bottom of this page. Build and run the starter app:

The starter app

The app is pre-populated with some folks you’ve supposedly encountered and wanted to remember. Click on Bob, Joan and/or Sam, and you’ll see that their physical features, likes and dislikes are specified in the cells below the preview.

Tapping Bob in PeopleListViewController (left) opens PersonDetailViewController (right). The screenshot series on the right shows PersonDetailViewController‘s scrolled page contents.

To understand the starter code, click through the project files and read the comments throughout. It’ll be your job in this tutorial to program the ability to add and edit your contacts.

Making Changes

What if Sam shaves his mustache or Joan starts wearing glasses? Or, during a particularly harsh winter, Bob decides that he dislikes everything including the weather? It’s useful to be able to update the people in People Keeper in real time.

Implementing selection behaviors

To start, if you choose a new feature or topic in PersonDetailViewController, the preview should update. To do this, at the bottom of the extension marked by UICollectionViewDelegate and UICollectionViewDataSource in PersonDetailViewController.swift, add:

override func collectionView(_ collectionView: UICollectionView,
                             didSelectItemAt indexPath: IndexPath) {
// 1
  switch Section(at: indexPath) {
// 2
  case .hairColor:
    person.face.hairColor = Person.HairColor.allCases[indexPath.row]
  case .hairLength:
    person.face.hairLength = Person.HairLength.allCases[indexPath.row]
  case .eyeColor:
    person.face.eyeColor = Person.EyeColor.allCases[indexPath.row]
// 3
  case .glasses:
    person.face.glasses = true
// 4
  case .facialHair:
    person.face.facialHair.insert(Person.FacialHair.allCases[indexPath.row])
// 5
  case .likes:
    person.likes.insert(Person.Topic.allCases[indexPath.row])
    person.dislikes.remove(Person.Topic.allCases[indexPath.row])
  case .dislikes:
    person.dislikes.insert(Person.Topic.allCases[indexPath.row])
    person.likes.remove(Person.Topic.allCases[indexPath.row])
  default:
    break
  }
// 6
  collectionView.reloadData()
}

Upon cell selection, the following happens:

  1. Using a switch statement, you execute the case that matches the enumeration value corresponding to the current section.
  2. If the user selects a hair color, set person‘s hair color to the Person.HairColor value at the selected row of the index path. If the user selects a hair length or eye color, set the hair length or eye color as well.
  3. When the user taps the glasses option, person‘s glasses Boolean becomes true.
  4. facialHair is a set since it can contain multiple items. When the user selects a facial hair style, insert it into the facial hair set.
  5. If the user selects a topic in the likes or dislikes section, add it to the likes or dislikes set respectively. Furthermore, a topic can’t be both liked and disliked, so if the user likes an item, deselect its cell in the dislike section and remove it from the dislike set and vice versa.
  6. Update the preview and selection UI by reloading the collection view.

Implementing deselection behaviors

Next, implement the deselection behaviors. Below the collectionView(_:didSelectItemAt:), add:

// 1
override func collectionView(_ collectionView: UICollectionView,
                             shouldDeselectItemAt indexPath: IndexPath) -> Bool {
  switch Section(at: indexPath) {
  case .facialHair, .glasses, .likes, .dislikes:
    return true
  default:
    return false
  }
}

override func collectionView(_ collectionView: UICollectionView,
                             didDeselectItemAt indexPath: IndexPath) {
    
  switch Section(at: indexPath) {
  // 2
  case .facialHair:
    person.face.facialHair.subtract([Person.FacialHair.allCases[indexPath.row]])
  case .likes:
    person.likes.subtract([Person.Topic.allCases[indexPath.row]])
  case .dislikes:
    person.dislikes.subtract([Person.Topic.allCases[indexPath.row]])
  case .glasses: // 3
    person.face.glasses = false
  default:
    break
  }
  collectionView.reloadData()
}

Here’s what you’re doing in each delegate method:

  1. Here, you specify that only the selected facial hair, glasses, likes and dislikes should be deselectable upon repeated tap. Deselection in any other section should only happen when the user selects another item in that same category.
  2. When the user deselects a facial hair style, likes or dislikes, you remove that deselected item from its respective set.
  3. When the user deselects the glasses feature, you set the glasses Boolean to false.

Build and run the app. You should now see the desired selection behaviors:

Previewing selections

You’ve now proven yourself a worthy commander of this powerful People Keeper technology. You’ve earned the right to wield your new weapon. On that day when a rival developer catches a glimpse of your People Keeper app, one powerful class will rise from the foundation to guard your states and protect against your evil competition…

SuperUndoManager

Introducing UndoManager

UndoManager is a general-purpose undo stack that is capable of simplifying your app’s state management. It can store whatever object or UI states that you’d like to track along with a closure, method or invocation capable of traversing back and forth through those states. Although it simplifies undo/redo when implemented properly, a lesser rival developer will likely implement UndoManager in a way that leads to fatal bugs. The two undo stacks that follow demonstrate a flawed example and a more successful example.

Undo stack example #1

People model and UndoManager stack

Undo Stack #1

Undo Stack #1 is a sequence of small steps that are each responsible for modifying the model and then the view to match. Though this strategy could work in theory, as the list of operations grows, errors become more likely because precisely matching each change in the model to each change in the view becomes increasingly difficult.

To understand why, here’s an exercise:

  1. What does the model look like after you pop the first undo operation off the stack?

    [spoiler title=”Answer #1″]Bob, Sam[/spoiler]

  2. And the second?

    [spoiler title=”Answer #2″]Bob, Kathy[/spoiler]

  3. And the third?

    [spoiler title=”Answer #3″]Bob, Kathy, Mike[/spoiler]

Whether or not you got those answers right, perhaps you can imagine how multiple insertions and deletions can complicate the index calculation of following insertions, deletions or updates. This undo stack is order-dependent and mistakes can cause inconsistencies between your data model and view. Does this error sound familiar?

NSInternalInconsistencyException

Model-view inconsistencies cause NSInternalInconsistencyExceptions.

Undo stack example #2

To avoid the error in Undo stack example #1, instead of recording data model and UI changes separately, record entire models:

Updated People model and UndoManager stack

Undo Stack #2

To undo an operation, you can replace the current model with a model on the undo stack. Undo Stacks #1 and #2 do the same thing, but #2 is order-independent and thus less error prone.

Undoing Detail View Changes

At the bottom of PersonDetailViewController.swift, insert:

// MARK: - Model & State Types

extension PersonDetailViewController {
// 1
  private func personDidChange(from fromPerson: Person) {
// 2
    collectionView?.reloadData() 
// 3
    undoManager.registerUndo(withTarget: self) { target in
      let currentFromPerson: Person = self.person
      self.person = fromPerson
      self.personDidChange(from: currentFromPerson)
    }
// 4
    // Update button UI 
    DispatchQueue.main.async {
      self.undoButton.isEnabled = self.undoManager.canUndo
      self.redoButton.isEnabled = self.undoManager.canRedo
    }
  }
}

Here’s what going on above:

  1. personDidChange(from:) takes the previous version of person as a parameter.
  2. Reloading the collection updates the preview and cell selections.
  3. undoManager registers an undo operation which, when invoked, sets self.person to the previous Person then calls personDidChange(from:) recursively. personDidChange(from:) updates the UI and registers the undo’s undo, i.e., it registers a redo path for the undone operation.
  4. If undoManager is capable of an undo — i.e., canUndo, enable the undo button — otherwise, disable it. It is the same for redo. While the code is running on the main thread, the undo manager doesn’t update its state until after this method returns. Using the DispatchQueue block allows the UI update to wait until this undo/redo operation completes.

Now, at the top of both collectionView(_:didSelectItemAt:) and collectionView(_:didDeselectItemAt:), add:

let fromPerson: Person = person

to retain an instance of the original person.

At the end of those same delegate methods, replace collectionView.reloadData() with:

personDidChange(from: fromPerson)

in order to register an undo that reverts to fromPerson. You removed collectionView?.reloadData() because that is already called in personDidChange(from:), so you don’t need to do it twice.

In undoTapped(), add:

undoManager.undo()

and in redoTapped(), add:

undoManager.redo()

to trigger undo and redo respectively.

Implementing shaking to undo/redo

Next, you’ll add the ability to shake the device running the app to initiate undo/redo. At the bottom of viewDidAppear(_:), add:

becomeFirstResponder()

at the bottom of viewWillDisappear(_:), add:

resignFirstResponder()

then below viewWillDisappear(_:), add:

override var canBecomeFirstResponder: Bool {
  return true
}

When the user shakes his or her device running the app to undo/redo, NSResponder goes up the responder chain looking for a next responder that returns an NSUndoManager object. When you set PersonDetailViewController as the first responder, its undoManager will respond to a shake gesture with the option to undo/redo.

Build and run your app. To test your changes, navigate to PersonDetailViewController, switch between a few different hair colors, and then tap or shake to undo/redo:

Adding Undo and Redo

Notice that tapping undo/redo doesn’t change the preview.

To debug, add the following within the top of the registerUndo(withTarget:handler:) closure:

print(fromPerson.face.hairColor)
print(self.person.face.hairColor)

Again, build and run your app. Try changing a person’s hair color a few times, undoing and redoing. Now, look at the debug console and you should see that, whenever you undo/redo, both print statements output only the final selected color. Is UndoManager dropping the ball already?

Not at all! The issue is elsewhere in the code.

Improving Local Reasoning

Local reasoning is the concept of being able to understand sections of code independent from context.

In this tutorial, for example, you’ve used closures, lazy initialization, protocol extensions and condensed code paths to make portions of your code understandable without venturing far outside their scopes – when viewing only “local” code, for example.

What does this have to do with the bug you’ve just encountered? You can fix the bug by improving your local reasoning. By understanding the difference between reference and value types, you’ll learn how to maintain better local control of your code.

Reference Types vs. Value Types

Reference and value are the two “type” categories in Swift. For types with reference semantics, such as a class, different references to the same instance share the same storage. Value types, however — such as structs, enums and tuples — each hold their own separate data.

To understand how this contributes to your current conundrum, answer the following questions using what you’ve just learned about reference vs. value type data storage:

  1. If Person is a class:
    var person = Person()
    person.face.hairColor = .blonde
    var anotherPerson = person
    anotherPerson.face.hairColor = .black
    
    person.face.hairColor == ??
    

    [spoiler title=”person.face.hairColor == ??”]

    person.face.hairColor == .black

    [/spoiler]

  2. If Person is a struct:
    var person = Person()
    person.face.hairColor = .blonde
    var anotherPerson = person
    anotherPerson.face.hairColor = .black
    
    person.face.hairColor == ??
    

    [spoiler title=”person.face.hairColor == ??”]

    person.face.hairColor == .blonde

    [/spoiler]

The reference semantics in question one hurts local reasoning because the value of the object can change underneath your control and no longer make sense without context.

So in Person.swift, change class Person { to:

struct Person {

so that Person now has value semantics with independent storage.

Build and run your app. Then, change a few features of a person, undoing and redoing to see what happens:

Fixed undo and redo

Undoing and redoing selections now works as expected.

Next, add the ability to undo/redo name updates. Return to PersonDetailViewController.swift and, within the UITextFieldDelegate extension, add:

func textFieldDidEndEditing(_ textField: UITextField) {
  if let text = textField.text {
    let fromPerson: Person = person
    person.name = text
    personDidChange(from: fromPerson)
  }
}

When the text field finishes editing, set person‘s new name to the field’s text and register an undo operation for that change.

Build and run. Now, change the name, change characteristics, undo, redo, etc. Mostly everything should work as planned but you may notice one small issue. If you select the name field then press return without making any edits, the undo button becomes active, indicating that an undo action was registered to undoManager even though nothing actually changed:

Undo with no actual changes

In order to fix this, you could compare the original and updated names, and register the undo only if those two values don’t match, but this is poor local reasoning — especially as person‘s property list grows, it’s easier to compare entire person objects instead of individual properties.

At top of personDidChange(from:), add:

if fromPerson == self.person { return }

Logically, it seems as if this line should compare the old and new person but there’s an error:

Binary operator '==' cannot be applied to operands of type 'Person' and 'Person!'

As it turns out, there’s no built-in way to compare Person objects since several of their properties are composed of custom types. You’ll have to define the comparison criteria on your own. Luckily, struct offers an easy way to do that.

Making Your Struct Equatable

Make your way over to Person.swift and make Person conform to Equatable by adding the following extension:

// MARK: - Equatable

extension Person: Equatable {
  static func ==(_ firstPerson: Person, _ secondPerson: Person) -> Bool {
    return firstPerson.name == secondPerson.name &&
      firstPerson.face == secondPerson.face &&
      firstPerson.likes == secondPerson.likes &&
      firstPerson.dislikes == secondPerson.dislikes
  }
}

Now, if two Person objects share the same name, face, likes and dislikes, they are “equal”; otherwise, they’re not.

Note: You can compare the Face and Topic custom objects within ==(_:_:) without making Face and Topic Equatable since each object is composed solely of Strings, which are inherently equatable objects in Swift.

Navigate back to PersonDetailViewController.swift. Build and run. The if fromPerson == self.person error should have disappeared. Now that you’ve finally gotten that line to work, you’ll soon delete it entirely. Using a diff instead will improve your local reasoning.

Creating Diffs

In programming, a “diff” compares two objects to determine how or whether they differ. By creating a diff value type, (1) the original object, (2) the updated object and (3) their comparison can all live within a single, “local” place.

Within the end of the Person struct in Person.swift, add:

// 1
struct Diff {
  let from: Person
  let to: Person
    
  fileprivate init(from: Person, to: Person) {
    self.from = from
    self.to = to
  }
// 2
  var hasChanges: Bool {
    return from != to
  }
}
// 3
func diffed(with other: Person) -> Diff {
  return Diff(from: self, to: other)
}

This code does the following:

  1. struct Diff holds both the original (from) and new (to) person values.
  2. If from and to are different, hasChanges is true; otherwise it’s false.
  3. diffed(with:) returns a Diff containing self’s Person (from) and the new person (to).

In PersonDetailViewController, replace the line private func personDidChange(from fromPerson: Person) { with:

private func personDidChange(diff: Person.Diff) {

It now takes the entire Diff and not just the “from” object as a parameter.

Then, replace if fromPerson == self.person { return } with:

guard diff.hasChanges else { return }

to use diff‘s hasChanges property.

Also remove the two print statements you added earlier.

Improving Code Proximity

Before replacing the now invalid calls to personDidChange(from:) with calls to personDidChange(diff:), take a look at collectionView(_:didSelectItemAt:) and collectionView(_:didDeselectItemAt:).

In each method, notice that the variable to hold the original person object is initialized at the top of the class, but not used until the bottom. You can improve local reasoning by moving the object creation and use closer together.

Above personDidChange(diff:), add a new method within its same extension:

// 1
private func modifyPerson(_ mutatePerson: (inout Person) -> Void) {
  // 2
  var person: Person = self.person
  // 3
  let oldPerson = person
  // 4
  mutatePerson(&person)
  // 5
  let personDiff = oldPerson.diffed(with: person)
  personDidChange(diff: personDiff)
}

Here’s what’s happening step by step:

  1. modifyPerson(_:) takes in a closure that receives a pointer to a Person object.
  2. var person holds a mutable copy of the class’s current Person.
  3. oldPerson holds a constant reference to the original person object.
  4. Execute the (inout Person) -> Void closure you created at modifyPerson(_:)‘s call site. The code in the closure will mutate the person variable.
  5. Then personDidChange(diff:) updates the UI and registers an undo operation capable of reverting to the fromPerson data model.

To invoke modifyPerson(_:), in collectionView(_:didSelectItemAt:), collectionView(_:didDeselectItemAt:), and textFieldDidEndEditing(_:) replace let fromPerson: Person = person with:

modifyPerson { person in

Replace personDidChange(from: fromPerson) with:

}

in order to condense the code using the modifyPerson(_:) closure.

Similarly, within undoManager‘s registerUndo closure, replace let currentFromPerson: Person = self.person with:

target.modifyPerson { person in

Replace self.personDidChange(from: fromPerson) with:

}

to simplify the code with a closure. This design approach centralizes our update code and thus preserves “locality of reasoning” for our UI.

Select all the code in the class, then navigate to Editor > Structure > Re-Indent to properly realign the new closures.

Then, in personDidChange(diff:), after guard diff.hasChanges else { return } and before collectionView?.reloadData() add:

person = diff.to

This sets the class’ person to the updated person.

Likewise, inside the target.modifyPerson { person in ... } closure replace self.person = fromPerson with:

person = diff.from

This restores the previous person when undoing.

Build and run. Check a person’s detail view and everything should work as expected. Your PersonDetailViewController code is complete!

Celebrating Undo and Redo

Now, tap the < PeopleKeeper back button. Uh-oh… Where did those changes go? You’ll have to pass those updates back to PeopleListViewController somehow.

Updating the People List

Within the top of the PersonDetailViewController class, add:

var personDidChange: ((Person) -> Void)?

Unlike the personDidChange(diff:) method, the personDidChange variable will hold a closure that receives the updated person.

At the end of viewWillDisappear(_:), add:

personDidChange?(person)

When the view disappears upon returning to the main screen, the updated person will return to the closure.

Now you’ll need to initialize that closure.

Back in PeopleListViewController, scroll to prepare(for:sender:). When transitioning to a selected person’s detail view, prepare(for:sender:) currently sends a person object to the destination controller. Similarly, you can add a closure to that same function to retrieve a person object from the destination controller.

At the end of prepare(for:sender:), add:

detailViewController?.personDidChange = { updatedPerson in
  // Placeholder: Update the Data Model and UI
}

This initializes detailViewController‘s personDidChange closure. You will eventually replace the placeholder comment with code to update the data model and UI; before that, there’s some setup to do.

Open PeopleModel.swift. At the end of class PeopleModel, but inside the class, add:

struct Diff {
// 1
  enum PeopleChange {
    case inserted(Person)
    case removed(Person)
    case updated(Person)
    case none
  }
// 2 
  let peopleChange: PeopleChange
  let from: PeopleModel
  let to: PeopleModel
    
  fileprivate init(peopleChange: PeopleChange, from: PeopleModel, to: PeopleModel) {
    self.peopleChange = peopleChange
    self.from = from
    self.to = to
  }
}

Here’s what this code does:

  1. Diff defines a PeopleChange enum, which indicates 1. Whether the change between from and to is an insertion, removal, update or nothing and 2. Which Person was inserted, deleted, or updated.
  2. Diff holds both the original and updated PeopleModels and the diff’s PeopleChange.

To help figure out which person was inserted, deleted or updated, add this function after the Diff struct:

// 1
func changedPerson(in other: PeopleModel) -> Person? {
// 2
  if people.count != other.people.count {
    let largerArray = other.people.count > people.count ? other.people : people
    let smallerArray = other.people == largerArray ? people : other.people
    return largerArray.first(where: { firstPerson -> Bool in
      !smallerArray.contains(where: { secondPerson -> Bool in
        firstPerson.tag == secondPerson.tag
      })
    })
// 3
  } else {
    return other.people.enumerated().compactMap({ index, person in
      if person != people[index] {
        return person
      }
      return nil
    }).first
  }
}

Here’s a breakdown of this code:

  1. changedPerson(in:) compares self’s current PeopleModel with the people model passed in as a parameter, then returns the inserted/deleted/updated Person if one exists.
  2. If one array is smaller/larger than the other, find the larger of the two arrays, then find the first element in the array not contained within the smaller array.
  3. If the arrays are the same size, then the change was an update as opposed to an insertion or deletion; in this case, you iterate through the enumerated new people array and find the person in the new array who doesn’t match the old one at the same index.

Below changedPerson(in:), add:

// 1
func diffed(with other: PeopleModel) -> Diff {
  var peopleChange: Diff.PeopleChange = .none
// 2
  if let changedPerson = changedPerson(in: other) {
    if other.people.count > people.count {
      peopleChange = .inserted(changedPerson)
    } else if other.people.count < people.count {
      peopleChange = .removed(changedPerson)
    } else {
      peopleChange = .updated(changedPerson)
    }
  }
//3
  return Diff(peopleChange: peopleChange, from: self, to: other)
}

Reviewing the above code:

  1. You initialize peopleChange to none to indicate no change. You will eventually return peopleChange from this method.
  2. If the new array is larger than the old array, changedPerson was inserted; if the new array is smaller, changedPerson was removed; if the new array is the same size as the old array, changedPerson was updated. In each case, use the person returned from changedPerson(in:) as changedPerson's parameter.
  3. You return the Diff with peopleChange, the original PeopleModel and the updated PeopleModel.

Now, at the bottom of PeopleListViewController.swift, add:

// MARK: - Model & State Types

extension PeopleListViewController {
// 1
  private func peopleModelDidChange(diff: PeopleModel.Diff) {
// 2
    switch diff.peopleChange {
    case .inserted(let person):
      if let index = diff.to.people.index(of: person) {
        tableView.insertRows(at: [IndexPath(item: index, section: 0)], with: .automatic)
      }
    case .removed(let person):
      if let index = diff.from.people.index(of: person) {
        tableView.deleteRows(at: [IndexPath(item: index, section: 0)], with: .automatic)
      }
    case .updated(let person):
      if let index = diff.to.people.index(of: person) {
        tableView.reloadRows(at: [IndexPath(item: index, section: 0)], with: .automatic)
      }
    default:
      return
    }
// 3
    peopleModel = diff.to
  }
}

Like personDidChange(diff:) in PersonDetailViewController, peopleModelDidChange(diff:) does the following:

  1. peopleModelDidChange(diff:) takes PeopleModel.Diff as a parameter, then it updates the UI based on the changes in the data model.
  2. If diff's peopleChange is an insertion, insert a table view row at the index of that insertion. If peopleChange is a deletion, delete the table view row at the index of that deletion. If peopleChange is an update, reload the updated row. Otherwise, if there was no change, exit the method without updating the model or UI.
  3. Set the class's peopleModel to the updated model.

Next, just as you added modifyPerson(_:) in PersonDetailViewController, add: modifyModel(_:) above peopleModelDidChange(diff:):

// 1
private func modifyModel(_ mutations: (inout PeopleModel) -> Void) {
// 2
  var peopleModel = self.peopleModel
// 3   
  let oldModel = peopleModel
// 4  
  mutations(&peopleModel)
// 5
  tableView.beginUpdates()
// 6
  let modelDiff = oldModel.diffed(with: peopleModel)
  peopleModelDidChange(diff: modelDiff)
// 7    
  tableView.endUpdates()
}

Here's what this code does step by step:

  1. modifyModel(_:) takes in a closure that accepts a pointer to a variable PeopleModel.
  2. var peopleModel holds a mutable copy of the class' peopleModel.
  3. oldModel holds a constant reference to the original model.
  4. Perform the mutations on the old model to produce the new model.
  5. Begin the series of tableView changes.
  6. peopleModelDidChange(diff:) executes the tableView insertion, deletion, or reload as determined by modelDiff peopleChange.
  7. End the table view updates.

Back in prepare(for:sender:), replace the placeholder comment with:

self.modifyModel { model in
  model.people[selectedIndex] = updatedPerson
}

to swap the person at the selected index with his or her updated version.

One final step. Replace class PeopleModel { with:

struct PeopleModel {

Build and run. Select a person's detail view, make some changes and then return to the people list. The changes now propagate:

Propagating changes

Next, you'll add the ability to delete and add people to your people table.

To process deletions, replace the placeholder comment in tableView(_:editActionsForRowAt:) with:

self.modifyModel { model in
  model.people.remove(at: indexPath.row)
}

to remove the person at the deleted index from both the data model and UI.

To handle insertions, add the following to addPersonTapped():

// 1
tagNumber += 1
// 2
let person = Person(name: "", face: (hairColor: .black, hairLength: .bald, eyeColor: .black, facialHair: [], glasses: false), likes: [], dislikes: [], tag: tagNumber)
// 3
modifyModel { model in
  model.people += [person]
}
// 4
tableView.selectRow(at: IndexPath(item: peopleModel.people.count - 1, section: 0), 
                    animated: true, scrollPosition: .bottom)
showPersonDetails(at: IndexPath(item: peopleModel.people.count - 1, section: 0))

Here, you do the following:

  1. The class variable tagNumber keeps track of the highest tag in the people model. As you add each new person, increment tagNumber by 1.
  2. A new person originally has no name, no likes nor dislikes, and a default face configuration. His or her tag value equals the current tagNumber.
  3. Add the new person to the end of the data model and update the UI.
  4. Select the row of the new item — i.e. the final row — and transition to that person's detail view so the user can set the details.

Build and run. Add people, update, etc. You should now be able to add and delete users from the people list and updates should propagate back and forth between controllers:

Add and delete users

You're not done yet — PeopleListViewController's undo and redo aren't functional. Time to code one last bit of counter-sabotage to protect your contact list!

Undoing People List Changes

At the end of peopleModelDidChange(diff:), add:

// 1
undoManager.registerUndo(withTarget: self) { target in
  // 2
  target.modifyModel { model in
    model = diff.from
  }
}
// 3
DispatchQueue.main.async {
  self.undoButton.isEnabled = self.undoManager.canUndo
  self.redoButton.isEnabled = self.undoManager.canRedo
}

Here, you:

  1. Register an undo operation capable of undoing data model and UI changes.
  2. Modify the people model by replacing the current model with the previous one.
  3. Enable/disable undoButton and redoButton appropriately.

In undoTapped(), add:

undoManager.undo()

and in redoTapped(), add:

undoManager.redo()

to trigger undo and redo respectively.

Last but not least, add shake to undo/redo to this controller. At the end of viewDidAppear(_:), add:

becomeFirstResponder()

At the end of viewWillDisappear(_:), add:

resignFirstResponder()

And beneath viewWillDisappear(_:), add:

override var canBecomeFirstResponder: Bool {
  return true
}

so that the controller can undo/redo in response to the shake gesture.

That's it! Build and run. You can edit, add, undo, redo, shake, etc.

You did it!

Where to Go From Here?

Download the final project using the Download Materials link at the bottom or top of this tutorial to see how it compares to your version.

To further explore the UndoManager API, try grouping undo features, naming undo actions, making undo operations discardable and using the various built-in notifications.

To further explore value types, try adding properties to Person and PeopleModel to make your app more robust.

And if you want to make your PeopleKeeper really work for you, add data persistence between app launches. See our "Updated Course: Saving Data in iOS" for more information.

Have any questions, comments or suggestions? Join in the forum discussion below!