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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
UndoManager Tutorial: How to Implement With Swift Value Types
30 mins
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:
-
personDidChange(from:)
takes the previous version ofperson
as a parameter. - Reloading the collection updates the preview and cell selections.
-
undoManager
registers an undo operation which, when invoked, setsself.person
to the previousPerson
then callspersonDidChange(from:)
recursively.personDidChange(from:)
updates the UI and registers the undo’s undo, i.e., it registers a redo path for the undone operation. - 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 theDispatchQueue
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:
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:
[spoiler title=”person.face.hairColor == ??”]
[/spoiler]
[spoiler title=”person.face.hairColor == ??”]
[/spoiler]
- 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]
- 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]
var person = Person()
person.face.hairColor = .blonde
var anotherPerson = person
anotherPerson.face.hairColor = .black
person.face.hairColor == ??
person.face.hairColor == .black
var person = Person()
person.face.hairColor = .blonde
var anotherPerson = person
anotherPerson.face.hairColor = .black
person.face.hairColor == ??
person.face.hairColor == .blonde
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:
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:
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.