Group Group Group Group Group Group Group Group Group

raywenderlich.com Forums

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


This is a companion discussion topic for the original entry at https://www.raywenderlich.com/5229-undomanager-tutorial-how-to-implement-with-swift-value-types
1 Like

Looking forward to working through this tutorial when I upgrade to iOS12 and Xcode 10. Tried to run it in iOS 11, Xcode 9 but lot’s of errors. Great examples of how to use the new CaseIterable protocol in swift 4.2. Well done Lyndsey.

To run in Xcode 9.4.1, with iOS 11.4, you need to fix 37 errors. All but 2 are about the new CaseIterable protocol for enums.

First error is: iOS Deployment Target ‘12.0’ is newer than SDK ‘iOS 11.4’ in target People Keeper.
If you click the error, it takes you to the setting, which you can change to 11.4.

Second error: UICollectionView.elementKindSectionHeader is used twice in PersonDetailViewController. Change it to UICollectionElementKindSectionHeader.

The other 35 errors: change each of the six enums the way this one is changed:

  enum HairColor: String { //}, CaseIterable {
    case black = "black", brown = "brown", blonde = "blonde", red = "red", gray = "gray"
    static let allCases: [HairColor] = [black, brown, blonde, red, gray]
  }

That is, comment out the CaseIterable. Then add a static let allCases, as an array of the enum type, set to an array of all the values. Yes, the Topic one is pretty long. It will be nice to have CaseIterable.

That’s it. It should now compile and run. I was able to do the entire tutorial, and everything worked.

I would not have gone through the trouble of converting it if I did not think it was worthwhile, so I will now comment on the tutorial as well.

It’s a really excellent tutorial, in several ways, including good examples of code that are not related to undo-redo.

  1. Excellent comments in the code. They are written in complete sentences, often several lines long, and really explain what is going on and what the intentions are. That took time and effort, and is quite valuable.

  2. Very nice job factoring the code throughout the app. It is a good model of how to build something that is robust and flexible at the same time. The phrase “local reasoning” is new to me, but I believe it is what I would call “push as much logic as you can down into the data model.”

  3. The undo-redo capability suddenly looks not so hard to do. It is a really nice feature to have in an app, the kind of thing users always want. I am sure I will be checking back on my version of this.

I will stop gushing now. I am a bit wary of making everything structs to get value semantics, especially if photos or or other large objects are involved. I am tempted to see if I can get the same result with classes. The refactoring and “local reasoning” make it seem like it could still work out well.

You could also just install the Swift 4.2 toolchain in Xcode 9 and get CaseIterable that way.

Thank you @sgerrard for the hints on how to make this work in Xcode 9 and iOS 11.4. I was able to get the project up and running perfectly in 5 minutes.

I have a question about how the undoManager is declared inside the two ViewControllers in the project:

private let _undoManager = UndoManager()
override var undoManager: UndoManager {
    return _undoManager
}

The comments in the code say you need to override the built-in undoManager; but it’s not clear to me why. Couldn’t you use the built-in one without re-creating a new instance? Are you disabling any functionality by overriding the built-in one? Why not create a class var called myUndoManager and use that instead?

Thanks!

You mean like this?

let myUndoManager = UndoManager()

That’s exactly what I did in both view controllers. I even used that name. :slight_smile:

You only need to override the property if you want a new one with the same name.

I think it is preferable to have a local undoManager, if the actions are all specific to the current view, so they aren’t still there after the view is closed. I don’t think you lose anything doing that. I think the default one is at the App level, and would keep everything, which could get strange.

Thanks for the reply! I found a short snippet on another online article that talked about the scope of the undoManager so that makes complete sense now. :grinning:
.

And what about when the undo manager is not local to the view? I just posted this question to forums.developer.apple.com so we’ll see what answer I get there, but I’ve got a tab bar controller at the top, with a nav controller inside it, with a table view controller inside the nav controller. I want to put the undoManager in the tab bar controller, so that everything changed across all four tabs will go into a single undoManager, and when the tab bar goes away, I’ll write the document back out, if necessary.

But the docs for UIResponder.undoManager seem to be misleading. They say, essentially, that there’s a runtime stack of undoManagers, one for each view in the hierarchy, and when you access UIResponder.undoManager, it walks up the hierarchy until it finds an undoManager to use. They give the example of a UITextField having its own undoManager, separate from the view that contains it (which makes sense). But it doesn’t appear to work that way. UIResponder.undoManager is a computed property, so it certainly could be code that traverses the view hierarchy, but whenever I access it in the table view controller, I get nil

It is definitely there in a TableViewController based class. When are you checking it?

It is not present in viewDidLoad(); however it is present in viewDidAppear(). Maybe you are checking for it too soon?

Both of these print:

  override func viewDidAppear(_ animated: Bool) {
    if let _ = self.undoManager {
      print("hello undo")
    }
    if let _ = view.undoManager {
      print("hello undo")
    }
  }

Thank you for checking and determining that it works for you; there must be something wrong with my code setup.

I have canBecomeFirstResponder returning true in my UITabBarController, along with a computed undoManager that returns a private UndoManager object. Then I call beginFirstResponder() in viewDidAppear(). Then it segues to the nav controller which loads a UITableViewController subclass that tries to use undoManager.registerUndo() (this is Swift 4) and that’s where it’s nil.

But if it works for you, there must be something else I’m doing wrong.

I did notice that you’re accessing both self.undoManager and view.undoManager. Why both?

The documentation says, “You may add undo managers to your view controllers to perform undo and redo operations local to the managed view.” Maybe the word “local” is the problem? Perhaps I need to put the undoManager into my view instead? But that means subclassing the UITableView… I’m guessing you didn’t do that.

Perhaps the UIResponder.undoManager computed property only checks the view hierarchy and undo managers inside a controller are meant to be local to that controller?! That would explain what I’m seeing, but it doesn’t explain why your self.undoManager worked in the view controller pushed onto the navigation controller stack and mine didn’t.

Thanks for the help! I’ll create an empty project and play with it there and see if I can narrow it down. I’ll report back here if/when I get this solved.

Edit: I just did the same test — Tab Bar -> Nav Controller -> TableView Controller — in a new project and I got the same results you did, i.e. it worked. There’s clearly something weird going on elsewhere in my app. I just need to figure out what it is. I’m going to try these same print statements in my app and see what I get. Thanks again for your help! :+1:

Edit^2: It seems that the undoManager object does work in viewDidAppear() but not viewWillAppear(). I don’t understand why other methods later in the life cycle can’t seem to find it, but I now believe it does have something to do with the view life cycle. Cheers!

It’s bad form to reply to one’s own post, but I have an answer. The forums @ apple.com provided the details. Essentially, the dynamic lookup of the undoManager field is of views and view controllers that are in the view hierarchy and when I invoke the callback (the closure) on a superclass that is a view, that superclass object is not currently in the view hierarchy. (Apparently, views not visible are not in the hierarchy, although I didn’t find that specifically stated anywhere in the docs.)

This means I can move my call to viewDidDisappear() (meaning the top view is hidden and the layer behind it is visible and that’s where the undoManager is) and it will work, or I could leave it where it’s at and pass an undoManager object that is accessible via normal inheritance of properties and is not based on a dynamic lookup in some opaque table whose contents is (apparently) undocumented. :slightly_frowning_face:

Unfortunately, I want the undoManager object in the nav controller so that all pages that are part of the navigation stack would have access to it. But my view controller classes won’t be subclassing the nav controller, so inheritance won’t help either. It appears I’m going to have to implement my own “undoManager stack” and use that.

No promises, but if I find a better solution while implementing that one, I’ll come back here and post about it. Thanks for the discussion. :slight_smile:

1 Like

This tutorial is more than six months old so questions are no longer supported at the moment for it. Thank you!