UI States in Cocoa

A super simple approach

In the web world, especially in React projects, the state of the app is an essential ingredient. The idea is that all visual representation originates from the current state i.e. if the state is changed the UI is likely to do as well.

In the Cocoa world this pattern is usually does not matter if you start a new project. There is some API like UIStateRestoring, but it will require some extra work.

I sat down and tried to build something that is super simple and maintainable. It should provide the following features:

  1. UI should update if state changes
  2. It should work well with bindings
  3. There should be a way to back and forward navigation by using states
  4. It should be possible to store and restore states e.g. for app relaunch
  5. It should work per NSDocument

Ok, so first of all we need the state itself, I used my SeaObject implementation I wrote about before. Therefore it already covers the requirement “store and restore states” out of the box. Here an example:

@interface State : SeaObject
@property NSString *searchString;
@property NSString *currentViewID;
@end

In this simple example we would store a search string and a pointer to the currently visible view controller.

Now we need to put that state somewhere. NSDocument seems to be ideal for that. To access it from any view controller we create a sub class of NSViewController we will use throughout the project and add a property called document to it. The following code will set it for us:

- (void)viewWillAppear {
    [super viewWillAppear];
    self.document = self.view.window.windowController.document;
}

- (void)viewDidDisappear {
    self.document = nil;
    [super viewDidDisappear];
}

We can now access the state via self.document.state or even bind values to it. In the demo code we added a little helper for observing state changes, which can be used like this (for the keyPath trick see this blog article) :

[self observeKeyPath:keyPath(self.document.state.searchString) action:^(id newValue) {
    [self performCustomSearchWithString:document.state.searchString];
}];

That’s basically it, just the navigation is missing and this is super easy if we start to store the states into a custom NSUndoManager. These are the two methods needed:

- (void)restoreState:(State *)state {
    [self storeState];
    self.state = state;
}

- (void)storeState {
    if (![self.state isEqual:self.lastStoredState]) {
        self.lastStoredState = self.state.copy;
        [self.stateStack registerUndoWithTarget:self
         selector:@selector(restoreState:)
         object:self.lastStoredState];
    }
}

Now calling [self.stateStack undo] will restore the state to what it was when you called storeState the last time. This way you can have your marks for when it makes sense to store a state.

Source Code

In the example code on GitHub you will find some more additional stuff like setupController and cleanupController methods where to put the observers and do other stuff once the state becomes available or goes away. The example also contains code that shows how firstResponder can be restored.

I would love to get your feedback on that approach. Of course I’m not the first to reason about states, see e.g. obj.c App Architecture for other approaches.

Published on September 17, 2018

 
Back to posts listing