Manjusaka

Manjusaka

Implementing Undo Functionality in Swift

In the past few months, there have been a lot of blog posts about the dynamic features people would like to add to Swift. Swift has already become a language with quite a few dynamic features: it has generics, protocols, first-class functions, and a standard library with many functions that operate in a dynamic way, like map and filter (which means we can use safer and more flexible functions instead of KVC with strings). For most people, especially those who want to introduce reflection, this means they can observe and modify things at runtime.

In Swift, reflection is limited, but you can still do some dynamic things at runtime. For example, here's how you can dynamically generate dictionaries for NSCoding or JSON.

Today, we'll take a look at how to implement undo functionality in Swift. One way to do this is by using NSUndoManager, which is based on the reflection mechanism in Objective-C. By using structs, we can implement undo in our app in a different way. Before we start the tutorial, make sure you understand how structs work in Swift (most importantly, that they are copied by value).

First, let's create a struct called UndoHistory. Usually, there's a warning when creating an UndoHistory, saying that it only works when A is a struct. To store all the states, we need to put them in an array. When we make a change, we just push it onto the array, and when we want to undo, we pop it off. Usually, we want an initial state, so we need to create an initializer:

    struct UndoHistory<A> {
        private let initialValue: A
        private var history: [A] = []
        init(initialValue: A) {
            self.initialValue = initialValue
        }
    }

For example, if we want to provide undo functionality in a tableViewController using an array, we can create a struct like this:

    var history = UndoHistory(initialValue: [1, 2, 3])

For different scenarios, we can create different structs to implement undo:

    struct Person {
        var name: String
        var age: Int
    }
    var personHistory = UndoHistory(initialValue: Person(name: "Chris", age: 31))

Of course, we want to get and set the current state (in other words, we want to manipulate our history in real time). We can get our state from the last item in the history array, and if the array is empty, we return our initial value. We can change our current state by appending it to the history array.

    extension UndoHistory {
        var currentItem: A {
            get {
                return history.last ?? initialValue
            }
            set {
                history.append(newValue)
            }
        }
    }

For example, if we want to modify a person's age, we can easily do it by using a computed property:

    personHistory.currentItem.age += 1
    personHistory.currentItem.age // Prints 32

Of course, the undo method is not yet implemented. It's very simple to remove the last item from the array. Depending on your preference, you can throw an exception when the array is empty, but I didn't choose to do that.

    extension UndoHistory {
        mutating func undo() {
            guard !history.isEmpty else { return }
            history.removeLast()
        }
    }

It's easy to use:

    personHistory.undo()
    personHistory.currentItem.age // Prints 31 again

Of course, the UndoHistory we have now is based on a simple Person class. For example, if we want to implement undo functionality in a table view controller using an array, we can use properties to get the elements from the array:

    final class MyTableViewController<item>: UITableViewController {
        var data: UndoHistory<[item]>

        init(value: [Item]) {
            data = UndoHistory(initialValue: value)
            super.init(style: .Plain)
        }

        override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return data.currentItem.count
        }

        override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCellWithIdentifier("Identifier", forIndexPath: indexPath)
            let item = data.currentItem[indexPath.row]
            // configure `cell` with `item`
            return cell
        }

        override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
            guard editingStyle == .Delete else { return }
            data.currentItem.removeAtIndex(indexPath.row)
        }
    }

Another cool feature of structs is that we can freely use the observer pattern. For example, we can modify the value of data:

    var data: UndoHistory<[item]> {
        didSet {
            tableView.reloadData()
        }
    }

Even if we modify a deeply nested value in the array (e.g. data.currentItem[17].name = "John"), we can easily locate the modification using didSet. Of course, we might want to do something more convenient, like reloadData. For example, we can use the Changeset library to calculate the changes and animate them based on insertions, deletions, moves, etc.

Obviously, this approach has its drawbacks. For example, it saves the entire history of states, not just the differences between states. This approach only uses structs to implement undo (or more accurately, some features of structs). This means you don't need to read the runtime programming guide; you just need to have a good understanding of structs and generics.

  1. Providing a computed property items for data.currentItem to get and set the value is a good idea. It makes implementing methods like data-source and delegate easier.
  2. If you want to go further, there are some interesting ideas: adding redo functionality or editing functionality. You can implement them in the tableView. If you're naive enough to do this, you'll find that there are duplicate records in your undo history.
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.