iOS applications are usually built with MVC (Model – View – Controller) architecture, which introduces very important concept of separating actual data (Model Layer) and their presentation (View Layer), while the application logic (Controller Layer) stands between them.
View ← Controller → Model
With MVC you typically write most of the code in UIViewController
, which usually represents the Controller Layer. View Layer can be easily done in Interface Builder and Model Layer usually doesn’t need a lot of code. The UIViewControleler
then holds strong references to both View and Model objects and is responsible for setting them up, handling actions and listening to events.
The problem is, that this middle layer tends to hold too much code and this situation is then jokingly called Massive View Controller. When a single class sets up views, formats data values, handles user input and actions, listens for a bunch of notifications and does networking, you broke it. Of course, there are good ways of spliting code to several classes with different resposibilties and one of them is MVVM.
You may heard people mentioning MVVM (Model – View – ViewModel) architecture before. It is derived from MVC, but introduces some new concepts. First of all the ViewModel Layer, which has terrible name, so we renamed it to Design Layer.
View → Design → Model
Basic idea is that View Layer doesn’t interact with Model Layer directly, but via Design Layer. The ownership of View Layer is inverted, in contrast to MVC, so the Design Layer is independent of the View Layer. This has some important consequences:
- You may have multiple View Layers for a single Design Layer.
- You may define an abstract Design for a View and then use it with multiple concrete subclasses.
- Design Layer is testable, if you are into that sort of things.
- Desing Layer usually doesn’t use UI framework (UIKit or AppKit), so it may be built cross-platform, similarly to the Model Layer. This may not be strictly true, because these UI frameworks sometimes provide data classes, typically
UIImage
and some apps work withUIFont
andUIColor
too. However, if you need cross-platform Model Layer and Design Layer you can easily create your own data classes to represent those objects.
Important aspect of this architecture, is that the data pretty often changes in Model Layer first and these changes need to be propagated to higher layers:
View ⇠ Design ⇠ Model
This propagation of changes must be done indirectly, since lower layers don’t know about higher layers. To do this in large scale, you need a solution for creating reactive connections and bindings between them. Foundation provides powerful mechanism of Key Value Observing that simplifies things, but it’s not enough and you need more versatile solution.
The most popular reactive library is Reactive Cocoa, that implements functional reactive approach (FRP). We are using our own framework Objective-Chain, that is more focused on objective approach.
- View Layer –
UIView
andUIViewController
subclasses that care about colors, layout, fonts, icons, and animations. - Design Layer – Backing objects for View Layer that care about content (including localization), how the content changes in time, and application logic.
- Model Layer – Data objects with actual values, serialization, networking, persistency, and little of logic (mostly data conversions and some validation).
In the following chapters, I will describe how we implemented Design Layer in our application Bubu – Motivate your child. It was the first time we used this architecture in large scale in a public project. The app is free and requires no email or passwords to be used, so feel free to try how it works even if you are not the target audience.
I will also describe some features of Objective-Chain below each code example.
For every View Layer component, which is for example a screen (a. k. a. View Controller), button, cell or custom view, we created its abstract counterpart in Design Layer and then expose -initWithDesign:
and .design
property. View Controller subclasses in our case never expose anything else than their own designs:
@interface EditProfileDesign : Design
- (instancetype)initWithProfile:(Profile *)profile;
@property (readonly) Profile *profile;
//...
@end
@interface EditProfileViewController : ViewController
- (instancetype)initWithDesign:(EditProfileDesign *)design;
@property (readonly) EditProfileDesign *design;
@end
Design Layer components are then composed, so EditProfileDesign
exposed Button, Dialog and Inuput Designs as properties. In practice, Design object is created using a Model object and then the View Controller is created using the Design object:
EditProfileDesign *design = [[EditProfileDesign alloc] initWithProfile:profile];
EditProfileViewController *controller = [[EditProfileViewController alloc] initWithDesign:design];
// Present it or something...
I will return to this example in later, but let me first talk about reusable Design components…
ButtonDesign
represents an abstract tappable component of UI. This class knows what information to display and what to call once the tap action is received. It is a counterpart to several UIKit components: standard Buttons Bar Button Items, Table Cells or Tab Bar Items.
BOOL isVisible
: Whether the button should be displayed at all, typically bound toUIButton.hidden
, but other button types may need different implementation.@property BOOL isEnabled
: Whether the button is active, typically bound to.enabled
, but Table Cells need special implementation.BOOL isEmphasized
: Whether the button should be highlighted, typically in bold typeface.NSString *title
: Main title to be displayed. In our case, this is inherited fromDesign
superclass.NSString *subtitle
: Secondary title like in Table Cells, but other buttons don’t use it.UIImage *image
: An image to display. Some buttons cannot display both title and image, so in our case, the image have precedence.NSInteger badge
: A number to display in the button, typically Tab Bar Item.badgeValue
, but could also be in a Table Cell.
Button Designs have a property OCAMediator *callback
, which is a reactive producer from Objective-Chain. This object produces events with values and you can connect blocks or invocations to it. In this case, an event is sent everytime the button is tapped:
[buttonDesign.callback connectTo:OCAInvocation(self, performAction)];
We created categories on UIBarButtonItem
, standard UIButton
, UITableCell
, UITabBarItem
and our custom button-like view classes, that provide constructor methods taking Button Design. Example for UIBarButtonItem
:
+ (UIBarButtonItem *)barButtonItemWithDesign:(ButtonDesign *)design {
if ( ! design) return nil;
UIBarButtonItemStyle style = (design.isEmphasized? UIBarButtonItemStyleDone : UIBarButtonItemStylePlain);
UIBarButtonItem *barButton = [[self alloc] initWithTitle:design.title style:style target:nil action:nil];
// Simple bindings:
[OCAProperty(design, title, NSString) connectTo:OCAProperty(barButton, title, NSString)];
[OCAProperty(design, image, UIImage) connectTo:OCAProperty(barButton, image, UIImage)];
// Combining binding:
[[OCAHub allTrue:
OCAProperty(self, isEnabled, BOOL),
OCAProperty(self, isVisible, BOOL),
nil] connectTo:OCAProperty(self, enabled, BOOL)];
// Objective-Chain provides UIBarButtonItem.producer, which is a callback on tap:
[self.producer connectTo:design.callback];
return barButton;
}
OCAProperty
encapsulates KVO and KVC mechanism and is a part of Objective-Chain.- Method
-connectTo:
creates one-way binding between two objects and values from the receiver will be passed to the argument. OCAHub
is an object, that combines values from multiple producers together.+allTrue:
is convenience method to combine Booleans using logical AND.
Design of a View Controller may expose .updateButton
property, which is then created in the initializer:
ButtonDesign *updateButton = [ButtonDesign new];
updateButton.title = @"Update";
updateButton.isEmphasized = YES;
[updateButton.callback connectTo:OCAInvocation(self, updateModel)];
self->_updateButton = updateButton;
OCAInvocation
is a mechanism to create invocations and invoke them reactively. It is a macro with a target and a method.
In the View Controller class we then simply construct the Bar Button in its initializer:
self.navigationItem.rightBarButtonItem = [UIBarButtonItem barButtonItemWithDesign:self.design.updateButton];
In case we wanted standard Button, we could create it in -viewDidLoad
:
UIButton *doneButton = [UIButton buttonWithDesign:design.updateButtonDesign];
[self.view addSubview:doneButton];
DialogDesign
is an abstract component that holds a message for the user and optionally requires the user to make a decision. It is a counterpart to Action Sheets and Alert Views.
NSString *title
: Title to be displayed. In our case, this is inherited fromDesign
superclass.NSString *message
: The message which may describe an issue. Action Sheet appends this to the.title
.cancelTitle
: Title for cancelling button. Default is “Cancel”, when there is.actionTitle
, or “Dismiss”, when there is no action.actionTitle
: Optional title for non-cancelling button. We didn’t implement multiple actions, yet.isDestructive
: Whether the action has destructive meaning. Action Sheet supports descructive buttons, but Alert View simply appends exclamation mark to the.actionTitle
, for example: “Delete!”.
Similar to Buttons, Dialog Design exposes .cancelCallback
and .actionCallback
which are again Objective-Chain producers. They are triggered when the user taps Cancel or Action respectively.
Again, we created categories for UIAlertView
and UIActionSheet
, for example:
+ (UIAlertView *)alertViewWithDesign:(DialogDesign *)design {
if ( ! design) return nil;
UIAlertView *alert = [UIAlertView new];
// Connect properties:
[OCAProperty(design, title, NSString) connectTo:OCAProperty(alert, title, NSString)];
[OCAProperty(design, message, NSString) connectTo:OCAProperty(alert, message, NSString)];
// Did you know you can change these after displaying the alert?
[alert addButtonWithTitle:design.cancelTitle];
alert.cancelButtonIndex = 0;
NSString *actionButtonTitle = design.actionTitle;
if (design.isDestructive) {
actionButtonTitle = [actionButtonTitle stringByAppendingString:@"!"];
}
if (actionButtonTitle) {
[alert addButtonWithTitle:actionButtonTitle];
}
OCAWeakify(alert);
[alert setCompletionBlock:^(NSInteger buttonIndex){
OCAStrongify(alert);
if (buttonIndex == alert.cancelButtonIndex) {
[OCACommand send:nil to:design.cancelCallback];
}
else {
[OCACommand send:nil to:design.actionCallback];
}
}];
return alert;
}
OCAWeakify
andOCAStrongify
macros are used to avoid retain cycles.+[OCACommand send:to:]
is a way to manually trigger reactive callbacks.-[UIAlertView setCompletionBlock:]
is implemented as a simple category in our Essentials project.
Since dialogs, especially UIAlertView
, are used to report errors, we made special DialogDesign
constructor for NSError
:
+ (DialogDesign *)dialogDesignWithError:(NSError *)error;
Real implementation in our app is not trivial, but it basically formats .title
and .message
with strings provided by the error, for example:
Request timed out
Cannot update profile,
because request timed out.Make sure you are connected
to the Internet and try again.Cancel Retry
We’ve put networking down to the Model Layer, close to data classes. This means that these errors originate in this lowest layer and have limited information about the context. What is known at this level is the reason of failure, stored in .localizedFailureReason
of NSError
. This is transformed into localized string “request timed out” as seen in the example above. The second paragraph of the message comes from .localizedRecoverySuggestion
which is related to the reason.
In addition, our network requests expose string property .userActivity
, that is set from the responsible Design object that started the request. User activity is higher-level purpose of the request, in our case “update profile”. This means the final message is composed of partial information from two layers of the app and this makes the error message clear and concise.
Recovery attempting is an interesting mechanism. When request receives an error in Model Layer, it is propagated to Design Layer in a form of DialogDesign
and then to View Layer in a form of UIAlertView
. Here, the user have an ability to cancel or restart the request. After tapping “Retry” this action is being carried down back to the Model Layer, where the request restarts itself.
This Recovery Attempting mechanism is in fact built into NSError
, but is obscure and is not even used by UIKit. Objective-Chain provides OCAErrorRecoveryAttempter
that integrates recovery attempting with other reactive components. Failed requests simply configure Recovery Attempter to provide “Retry” option that invokes -start
method. Dialog Design constructor then connects its Action button to this Recovery Attempter and that's all. Works like magic.
Design of the View Controller may expose .deleteDialog
to confirm deletion:
DialogDesign *deleteDialogDesign = [DialogDesign new];
deleteDialogDesign.actionTitle = @"Delete photo";
deleteDialogDesign.isDestructive = YES;
[deleteDialogDesign.actionCallback connectTo:OCAInvocation(self, setPhoto:nil)];
self->_deleteDialog = deleteDialogDesign;
View Controller then instantiates Action Sheet and displays it. In our case as a reaction to long-press gesture:
UIActionSheet *sheet = [UIActionSheet actionSheetWithDesign:self.design.deleteDialog];
[sheet showInView:self.view];
However, dialogs are not usually “static” like in the example above. Take an example of the Error Dialogs mentioned above. Their dialog need to be displayed when the error occurs, but how do we let the View Controller know?
In that case, the View Controller’s Design exposes .errorDialogCallback
which produces DialogDesign
objects when they need to be displayed. We then connect request’s .error
property, convert to DialogDesign
and then send to this callback:
[[OCAProperty(request, error, NSError) transformValues:
[DialogDesign transformErrorIntoDialog],
nil] connectTo:self.errorDialogCallback];
+transformErrorIntoDialog
returnsNSValueTransformer
that invokes+dialogDesignWithError:
mentioned above, so it transformsNSError
toDialogDesign
.
In the View Controller, we then connect this callback to a block:
[self.design.errorDialogCallback subscribeForClass:[DialogDesign class] handler:^(DialogDesign *dialog) {
[[UIAlertView alertViewWithDesign:dialog] show];
}];
- Method
-subscribeForClass:handler:
is convenience method for connectingOCASubscriber
objects. These are simple block observers, but you need to provide input class for type safety.
So far, I’ve described reusable Design Layer components that abstracts Buttons and Dialogs. Now it’s time to use these reusable components and give them some real meaning.
For every View Controller subclass we create it’s Design counterpart, for example those EditProfileViewController
and EditProfileDesign
from earlier code example. Then we think about the properties, displayed content and active UI components. We create the Design without thinking about graphical appearance of the View Controller.
Let’s build this Edit Profile screen…
-initWithProfile:
: We need some initializer which usually takes a Model Layer object. Also, most of the implementation is in this method.Profile *profile
: Read-only property, where the Model obejct is stored.
NSString *title
: Every screen should be titled. In our case it is inherited fromDesign
superclass.NSString *username
: Username to be displayed somewhere.UIImage *profilePhoto
: Photo that should be displayed next to the username.
-
ButtonDesign *cancelButton
: Button that closes this screen. -
ButtonDesign *submitButton
: Button to sends updated profile to a server. -
OCAMediator *closeCallback
: Producer of “close events”. When this callback triggers, the screen should close.Closing could be a result of tapping Cancel Button or result of a successful update request. However, the View Controller doesn’t really care about why is it closing, only about how to close (pop, dismiss, etc.). This allows us, for example, later add a Dialog asking for unsaved changes when the user taps Cancel Button. In that case, Cancel Button would not trigger close, but the confirmation Dialog would do it instead.
ButtonDesign *deleteButton
: Button that deletes the user profile. Tapping this button is handled by View Controller and it presents confirmation Delete Dialog. (We should improve this by inverting this responsibility.)DialogDesign *deleteDialog
: Dialog that should appear after tapping Delete Button. It has a destructive action button that starts a network request. Successful deletion is also a reason to close the screen.
BOOL isBusy
: Whether there is some action running (a network request) and the UI should reflect this state. In our app, it’s usually connected from.isLoading
property of network requests. We resigned first responder, disabled interaction with the table view and disabled Navigation Bar buttons.OCAMediator *errorDialogCallback
: Producer ofDialogDesigns
containing information about errors that occur, just like described in previous chapter.
Now, some features I didn’t talk about yet:
-
StringInputDesign *usernameInput
: Design for entering username, typically represented using Text Field. -
DateInputDesign *birthdayInput
: Design for entering birth date, typically represented using Date Picker. -
NumberInputDesign *luckyNumberInput
: Design for entering your lucky number, for example using Stepper. I just wanted to demonstrate numeric input here :)These are subclasses of
InputDesign
and they handle formatting, validation, limits, default values, placeholders and they report editing changes and state. More on these in the next chapter.
Important thing to notice is also what is missing from the Design interface. We don’t expose action methods like -submitProfile
or -deleteProfile
, because these are connected to .submitButton
and .deleteDialog
respectively and are considered implementation detail to the View Layer.
Most of the implementation is in -initWithProfile:
where we also establish reactive connections between properties. I won’t include full implementation, but only some interesting parts.
Title of the screen will stay the same for all time while it is presented, so we just assign it once. On the other hand, .username
is going to change, since this is editing screen and we have .usernameInput
for entering new username. It needs to be connected “dynamically”:
self->_title = NSLocalizedString(@"Edit Profile", nil);
[[OCAProperty(self, profile.username, NSString)
replaceNilWith:NSLocalizedString(@"Loading...", nil)
connectTo:OCAProperty(self, username, NSString)];
-replaceNilWith:
is a convenience transformation method, that will substitute “Loading…” when the username is nil.
We need to create subdesigns for Button, Dialogs and then Inputs. There already was an example of this, but let’s look at it again. This time we connect Cancel button to the .closeCallback
and disable it when busy:
ButtonDesign *cancelButton = [ButtonDesign new];
cancelButton.title = NSLocalizedString(@"Cancel", nil);
[[OCAProperty(self, isBusy, BOOL)
negateBoolean]
connectTo:OCAProperty(cancelButton, isEnabled, BOOL)];
[cancelButton.callback connectTo:self.closeCallback];
self->_cancelButton = cancelButton;
-negateBoolean
is a convenience method for transforming booleans.
When we set self.isBusy = YES
, this button gets disabled.
I already mentioned that View Controller’s have very small interface, just to expose the Design, so the only interesting part is the implementation.
First, we connect the title and create Cancel button. We do this in -initWithDesign:
method:
[OCAProperty(self.design, title, NSString) connectTo:OCAProperty(self, title, NSString)];
self.navigationItem.leftBarButton = [UIBarButtonItem barButtonItemWithDesign:self.design.cancelButton];
The Submit button wil be more complicated. We created two UIBarButtonItems
, one from .submitButton
design and the other with spinning indicator. Then we switch between those two based on .isBusy
property:
[[OCAProperty(self.design, isBusy, BOOL) transformValues:
[OCATransformer ifYes:loadingItem ifNo:submitButtonItem],
nil] connectTo:OCAInvocation(self.navigationItem, setLeftBarButtonItem:OCAPH(UIBarButtonItem) animated:YES)];
OCATransformer
is a factory forNSValueTransformer
objects with hundreds of factory methods.- One of the factory methods is
+ifYes:ifNo:
. This Transformer works similarly to ternary operator? :
. When the input value evaluates to true, first argument is returned, otherwise the second one. OCAInvocation
supports fixed and placeholder arguments. Placeholders are defined usingOCAPH
(short forOCAPlaceholder
) and in this case, theUIBarButtonItem
returned from Transformer will get substitued everytime the invocation is invoked.
Finaly we define how to close, connecting .closeCallback
to appropriate method:
[self.design.closeCallback connectTo:OCAInvocation(self, dismissViewControllerAnimated:YES completion:nil)];
Now, we move to -viewDidLoad
method to implement the rest of the connections. We disable user interaction when the screen is busy:
[[OCAProperty(self.design, isBusy, BOOL)
negateBoolean]
connectTo:OCAProperty(self.view, userInteractionEnabled, BOOL)];
We present any errors that occur:
[self.design.errorDialogCallback subscribeForClass:[DialogDesign class] handler:^(DialogDesign *dialog) {
[[UIAlertView alertViewWithDesign:dialog] show];
}];
We create required labels and image views and connect their content:
[OCAProperty(design, username, NSString) connectTo:OCAProperty(usernameLabel, text, NSString)];
[[OCAProperty(design, profilePhoto, UIImage)
replaceNilWith:defaultImage
connectTo:OCAProperty(photoView, image, UIImage)];
After we are done with this, the last part is setting up the form input fields. Let’s make a chapter break here…
Form fields need quite a lot of code around. From default values, placeholders, formatting, validation to reporting editing events. In our app, we used form screens pretty often, so it was inevitable to make them reusable. We have 3 input designs for strings, dates and numbers with a common superclass.
-
NSString *title
: Title of the input. Inherited fromDesign
superclass. -
BOOL isEnabled
: Whether this input should be active. -
BOOL isValid
: Whether the entered value is valid. Meaning of “valid” depends on usage, so this property is read/write and the owning Design is responsible to connect something to it. By default, it’s true. -
NSString *placeholder
: String displayed when the field is empty. Typically in Text Field, but useful for other types as well. -
OCAMediator *changeCallback
: Producer of change events that produces the latest value everytime it changes. -
OCAMediator *finishCallback
: Producer for finish event that produces the latest value. Typically when the Text Field resigns first responder.Owning Design can decide, whether it will connect change or finish callbacks to the Model property. For Text Fields we usually take the value on finish, for dates on every change.
NSString *string
: The actual value displayed in the Text Field (or Text View).NSString *defaultString
: String to be used as an initial value when.string
is nil.
String Input Designs are in our case instantiated into Table Cell subclass with one label and one text field. Most of the properties are connected to the Text Field. Objective-Chain provides reactive producers for UIControl
events, that are connected to .changeCallback
and .finishCallback
.
NSNumber *number
: Actual value that should be displayed.NSNumber *defaultNumber
: Number to be used as an initial value when.number
is nil.NSNumberFormatter *formatter
: In case you display the number in a text form.NSString *formattedNumber
: Read-only result of formatting.number
using.formatter
.
We use Number Inputs mostly with Steppers, but you could also use Slider or Text Field with numeric keyboard. In one case, we even use Date Picker in Countdown Mode to enter time interval.
NSDate *date
: Actual value to be displayed.NSDate *defaultDate
: Date to be used as an initial value when.date
is nil.NSDate *maximumDate
: Upper limit.NSDate *minimumDate
: Lower limit.UIDatePickerMode mode
: Specify Time style or Date style or bothNSDateFormatter *formatter
: Used to convert.date
to string. Default formatter is created based on.mode
property.NSString *formattedDate
: Read-only result of formatting.date
using.formatter
.
We used Date Input Designs exclusively with Date Pickers which are a bit tricky to do in Table Views. Tricky was also working with Date & Time, but as a result, we added some useful new Transformers to Objective-Chain. The OCATransformer
factory now provides Date transformers to:
- Convert between timestamps and Dates.
- Turn Dates to components and back, even relative to another Date.
- Add and subtract intervals and components.
- Round Dates to an arbitraray calendar units.
- Limit dates to maximum and minimum.
- Format and parse Dates using formatters.
We need to create instances of Input Design classes, here’s the username input:
StringInputDesign *usernameInput = [StringInputDesign new];
usernameInput.title = NSLocalizedString(@"Username", nil);
usernameInput.placeholder = NSLocalizedString(@"Required", nil);
[OCAProperty(self, profile.username, NSString) bindWith:OCAProperty(usernameInput, string, NSString)];
[[usernameInput.changeCallback transformValues:
[OCATransformer evaluatePredicate:[[OCAPredicate isEmpty] negate]],
nil] connectTo:OCAProperty(usernameInput, isValid, BOOL)];
self->_usernameInput = usernameInput;
-bindWith:
is a method that creates bi-directional connection between two properties. When one of them changes, the other is set to the same value.OCAPredicate
is a factory forNSPredicate
instances.
Here’s an example of birthday input:
DateInputDesign *birthdayInput = [[DateInputDesign alloc] initWithMode:UIDatePickerModeDate];
birthdayInput.title = NSLocalizedString(@"Birthday", nil);
birthdayInput.placeholder = NSLocalizedString(@"Optional", nil);
birthdayInput.maximumDate = [NSDate new];
[OCAProperty(self, profile.birthday, NSDate) bindWith:OCAProperty(birthdayInput, date, NSDate)];
self->_dateInputDesign = dateDesign;
And then we combine .isValid
properties together:
[[OCAHub allTrue:
OCAProperty(usernameInput, isValid, BOOL),
OCAProperty(birthdayInput, isValid, BOOL),
OCAProperty(luckyNumberInput, isValid, BOOL),
nil] connectTo:OCAProperty(self, isValid, BOOL)];
Property .isValid
may be connected to .isEnabled
of Submit Button, so it gets disabled, when the username is empty. Also, when user enters new username, usernameLabel.text
is automatically updated, because we connected them through profile.userneme
property.
The View Controller then need to create Table Cells to be used in static Table View. We have TextCell
and DateCell
classes with constructors that take appropriate Input Design. Here is an example of TextCell
:
+ (TextCell *)cellWithDesign:(StringInputDesign *)design {
if ( ! design) return nil;
TextCell *cell = [TextCell new];
[OCAProperty(design, title, NSString) connectTo:OCAProperty(cell, title, NSString)];
[OCAProperty(design, isEnabled, BOOL) connectTo:OCAProperty(cell, textField.enabled, BOOL)];
[OCAProperty(design, placeholder, NSString) connectTo:OCAProperty(cell, textField.placeholder, NSString)];
[OCAProperty(design, string, NSString) connectTo:OCAProperty(cell, textField.text, NSString)];
// Reactive producers for UIControl events:
[cell.textField.producerForText connectTo:design.changeCallback];
[cell.textField.producerForEndEditing connectTo:design.finishCallback];
if (design.string == nil && design.defaultString) {
cell.textField.text = design.defaultString;
}
return cell;
}
And that’s pretty much all. It should work now.
This architecture helped us to build stable and robust application pretty fast. We ended up with more classes, but they are smaller and have limited responsibilties. When something doesn’t work, you know exactly what connection to debug. Also, were able to split work on View and Design layers after we defined their interface.
When most of the things work reactively, your code runs exectly when it should. We spent very little time figuring out why are displayed values wrong, because they were always and instantly up-to-date.
On the other hand, using KVO and Invocations in this huge scale brings up a little performance penalty. Objective-Chain with KVO take around 20 % of launch time out of total 4.5 seconds, but the KVO registration is called hundreds of times.
We are definitely going to use View → Design → Model approach in future projects (where suitable) and for that we will need to move reusable parts from this app to a separate library. We plan to make it open-source, so stay tuned!
For better apps!
Martin Kiss