Skip to content

Instantly share code, notes, and snippets.

@Tricertops
Last active August 30, 2019 05:28
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Tricertops/bd038dfbf0663818b0ad to your computer and use it in GitHub Desktop.
Save Tricertops/bd038dfbf0663818b0ad to your computer and use it in GitHub Desktop.
How we used MVVM architecture and a reactive framework to build our latest iOS app, from theory to actual code examples.

View → Design → Model

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 with UIFont and UIColor too. However, if you need cross-platform Model Layer and Design Layer you can easily create your own data classes to represent those objects.

Bindings

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.

Responsibilities

  1. View LayerUIView and UIViewController subclasses that care about colors, layout, fonts, icons, and animations.
  2. Design Layer – Backing objects for View Layer that care about content (including localization), how the content changes in time, and application logic.
  3. 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.

Designs

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…

Buttons

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.

State

  • BOOL isVisible: Whether the button should be displayed at all, typically bound to UIButton.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 from Design 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.

Actions

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)];

Views

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.

Usage

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];

Dialogs

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.

State

  • NSString *title: Title to be displayed. In our case, this is inherited from Design 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!”.

Actions

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.

Views

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.

Errors

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

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.

Usage

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];

Real Time

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 returns NSValueTransformer that invokes +dialogDesignWithError: mentioned above, so it transforms NSError to DialogDesign.

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 connecting OCASubscriber objects. These are simple block observers, but you need to provide input class for type safety.

View Controllers

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…

Design Interface

Model Object

  • -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.

Content

  • NSString *title: Every screen should be titled. In our case it is inherited from Design superclass.
  • NSString *username: Username to be displayed somewhere.
  • UIImage *profilePhoto: Photo that should be displayed next to the username.

Navigation

  • 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.

Deletion

  • 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.

Activity

  • 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 of DialogDesigns containing information about errors that occur, just like described in previous chapter.

Forms

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.

Missing

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.

Design Implementation

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.

Static vs. Dynamic

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.

Subdesigns

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.

View Controller Implementation

I already mentioned that View Controller’s have very small interface, just to expose the Design, so the only interesting part is the implementation.

Navigation

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 for NSValueTransformer 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 using OCAPH (short for OCAPlaceholder) and in this case, the UIBarButtonItem 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)];

View

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…

Inputs

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.

Any Input

  • NSString *title: Title of the input. Inherited from Design 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.

String Input

  • 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.

Number Input

  • 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.

Date Input

  • 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 both
  • NSDateFormatter *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.

Usage

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 for NSPredicate 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.

Cells

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.

Conclusion

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.

Future

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment