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:
- UI should update if state changes
- It should work well with bindings
- There should be a way to back and forward navigation by using states
- It should be possible to store and restore states e.g. for app relaunch
- 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