Jan 31 2013

Cocoa UI Preservation Y’all

Mac OS X Lion introduced sudden termination, an opt-in feature for Cocoa apps that allows the system to terminate apps that aren’t in use and transparently re-launch them when the user brings them back to the foreground. This allows the system to keep resource usage as low as possible, but it also puts an additional burden on the developer to ensure that when the application is re-launched, it looks just like it did when the user last saw it. You’ve probably seen this in document-based applications that remember which documents were open when they last ran.

Fortunately the work of implementing this has been minimized by another Lion feature, Cocoa UI Preservation, which remembers the state of restorable windows so they can be recreated at launch. UI preservation takes care of the unpleasantness of this sort of meticulous work by recording and restoring the state of the windows and providing us with hooks to store our own data.

Let’s look at how we might implement UI preservation in our own project. Consider a typical Cocoa application with a preferences window. The AppDelegate handles showing the PreferencesWindowController:

- (PreferencesWindowController *)preferencesWindowController
{
    if (!_preferencesWindowController)
    {
        _preferencesWindowController =
            [[PreferencesWindowController alloc] init];
    }
    return _preferencesWindowController;
}

- (IBAction)showPreferences:(id)sender
{
    [[self preferencesWindowController] showWindow:nil];
}

Let’s implement UI preservation for the preferences window in our application. Cocoa automatically persists state information for all restorable windows in an application. So first we must set the preferences window to be restorable, as well as tell it which class knows how to restore it (more on that in a moment).

- (PreferencesWindowController *)preferencesWindowController
{
    if (!_preferencesWindowController)
    {
        _preferencesWindowController =
            [[PreferencesWindowController alloc] init];
        _preferencesWindowController.window.restorable = YES;
        _preferencesWindowController.window.restorationClass = [self class];
        _preferencesWindowController.window.identifier = @"preferences";
    }
    return _preferencesWindowController;
}

We can turn on Restorable and set the Identifier in Interface Builder, but as of Xcode 4.5.2, there isn’t a control for setting the restoration class. Thus it feels much cleaner to do it all in code. And although we use a string literal for the identifier in this blog entry, you’ll want to use a string constant in your own code.

Setting the restorable boolean is straightforward enough, but what about this restoration class? Cocoa will store this class along with the window’s properties, such as its frame. When it comes time to restore the window when our application starts, Cocoa will ask the restoration class to do this by sending it the message +restoreWindowWithIdentifier:state:completionHandler:. The identifier parameter will match the identifier we had set on the window. Let’s implement this method in the AppDelegate:

+ (void)restoreWindowWithIdentifier:(NSString *)identifier
                              state:(NSCoder *)state
                  completionHandler:(void (^)(NSWindow *, NSError *))completionHandler
{
    NSWindow *window = nil;
    if ([identifier isEqualToString:@"preferences"])
    {
        AppDelegate *appDelegate = [NSApp delegate];
        window = [[appDelegate preferencesWindowController] window];
    }
    completionHandler(window, nil);
}

This method is responsible for creating the window that corresponds to the given identifier and passing that NSWindow instance to the completion handler block. By not requiring that the developer return the window from this method, restoring the window may be delayed until an appropriate time in the future without blocking the rest of the application startup.

Note that by lazily creating our window controller via a read-only property, it becomes quite simple to restore this window, as well as to ensure that its restoration properties are configured.

Let’s review. In order for Cocoa to be able to restore a window:

  • The window must be marked restorable.
  • The window must have an identifier.
  • The window must have a restoration class.
  • Given an identifier, the restoration class must be able to recreate the corresponding window.

In this example, our restoration class is the application delegate, but it won’t always be. A good rule of thumb is that the restoration class will be the same class that keeps a strong reference to that window’s controller. You will probably need some mechanism to find the appropriate instance of this class from within the +restoreWindowWithIdentifier:state:completionHandler: class method. In this case we use the -[NSApplication delegate] property.

Not Just Frames

We’ve seen how to restore windows to their former position and size on the screen, but Cocoa actually restores more than this. For example, the window’s first responder is stored, as is the selected tab of an NSTabView. Data is not saved, however. So if we want to preserve the user’s input in a text field, or the position of a slider, we will need to make other arrangements. In the old days we might have used NSUserDefaults, but UI preservation gives us a much more convenient mechanism, as we shall see.

Suppose our preferences window has several panes. The window controller might have a property, selectedPaneIndex. Its setter would look something like this:

- (void)setSelectedPaneIndex:(NSUInteger)index
{
    NSViewController *paneViewController =
        [self paneViewControllerForIndex:index];
    [[self window] setContentView:[paneViewController view]];
    _selectedPaneIndex = index;
}

We’d like to ensure that the pane the user had last selected is shown when the window is restored. By implementing +restorableStateKeyPaths on the PreferencesWindowController we can easily cue Cocoa to save this state information along with the window:

+ (NSArray *)restorableStateKeyPaths
{
    return @[ @"selectedPaneIndex" ];
}

The class must be KVC- and KVO-compliant for the key paths returned by this method, as Cocoa observes the key paths for changes and saves the state as needed. Once the window has been passed to the completion block by the restoration class (shown in the first section), Cocoa will use KVC to restore the state.

If you prefer to have more control over which values are stored, and how they are restored, you can implement -encodeRestorableStateWithCoder: and -restoreStateWithCoder:. These methods will look very familiar if you’ve used Cocoa archiving. They can be implemented on the window itself, as well as a view, window controller or document. Similarly, the window’s delegate can implement -window:willEncodeRestorableState: and -window:didDecodeRestorableState:.

Let’s look at a case where we might want to use the aforementioned methods. +restorableStateKeyPaths depends on our setter doing the work of reflecting the state in the UI, but this isn’t always appropriate. Suppose we have a property, text, for which this is the case. Here’s how we might implement such a pattern:

- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
    [coder encodeObject:self.text forKey:@"text"];
}

- (void)restoreStateWithCoder:(NSCoder *)coder
{
    self.text = [coder decodeObjectForKey:@"text"];
    [self updateForChangedText];
}

We need one more bit of code, however, and that is to tell the UI preservation system when some state has changed which should be persisted. We use the method -invalidateRestorableState to do this:

- (void)setText:(NSString *)text
{
    _text = text;
    [self invalidateRestorableState];
}

The above step isn’t necessary when using +restorableStateKeyPaths because KVO handles it for us. Keep in mind that you don’t have to choose between +restorableStateKeyPaths and the archiving methods; they can be used together.

Not Everything Need Be Preserved

There are some pitfalls to watch out for with UI preservation. Because Restorable is checked in the Interface Builder editor by default, you may find that if you have run an app once and then later resize the main window in Interface Builder, the application will appear to have ignored your changes. What’s happening is that UI preservation is remembering the window size from last time it ran and overwriting the window size from the XIB.

There are a few ways to address this:

  • Quit the application with Command-Option-Q (Quit and Close All Windows), which causes the application to forget its UI preservation information.
  • Edit the Run action in Xcode’s Scheme Editor and check “Launch application without state restoration” in the Options tab. Just remember to turn it off later.
  • An application’s state restoration data is stored in the user’s library, under ~/Library/Saved Application State/. You can delete the app’s folder in order to reset it. This is effectively the same as “Quit and Close All Windows”.

It’s worth noting that UI preservation means more opportunities for tricky bugs, given that there is now additional state information associated with the application.

Back to the App

Lion was announced with the phrase “Back to the Mac.” UI preservation is a behavior that has long been thought of as important for iOS apps, which is becoming more important for Mac apps, particularly alongside sudden termination. If you write iOS apps, you’ll be happy to know that iOS has APIs that are quite similar to those we have discussed today for Cocoa. (It is somewhat ironic, however, that they weren’t available until iOS 6.)

For more on UI preservation, as well as links to the various APIs discussed here, check out the Mac App Programming Guide, under the heading “Support the Key Runtime Behaviors in Your Apps.” If you’re curious about state restoration on iOS, you’ll want to refer to the very detailed coverage in the iOS App Programming Guide. If you’re new to archiving, check out the Archives and Serializations Programming Guide, Chapter 10 in our Cocoa book or Chapter 14 in our iOS book.

2 Comments

  1. Mike Abdullah

    One quibble: I’m pretty sure the feature is “automatic termination”. “Sudden termination” was introduced in 10.6 and merely allows the system to quit apps quicker when they have no pending unsaved data/state

  2. Sasmito Adibowo

    An important oversight on Apple’s part is archiving NSViewController objects. As of 10.8.2 view controllers won’t get archived. If you encode an NSViewController object as part of your implementation of the window controller’s encodeRestorableStateWithCoder: method, you won’t get a view controller back in the window controller’s restoreStateWithCoder:.

Leave a Comment

Join the discussion. Do not worry, your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>