Skip to content
Samuel Défago edited this page Mar 30, 2015 · 10 revisions

Bindings hands-on

CoconutKit 3.0 introduces bindings for iOS, which are heavily inspired by Cocoa bindings, only available for Mac OS.

Overview

CoconutKit bindings provide a convenient and efficient way to bind (thus the name) a view displayed on screen to an underlying model object, connected to it by a given key path. With no or very few lines of code, bindings ensure that:

  • When the model changes, the bound view gets automatically updated
  • When the bound view changes, the model gets automatically updated. If a validation is associated with the key path, it can optionally be triggered automatically

A view can be bound to a given list of types it natively supports. For example, a UILabel natively supports strings. If the key path returns a string, everything is fine, otherwise you can always ensure that some proper type is provided to a bound view by applying some conversion first. This is the role of transformers.

Transformers provide one-way or two-way conversion between objects. A transformer can e.g. turn a number into a string. Another transformer can perform date formatting and parsing. A third one can simply accept a number as input and round it. When appropriate, such transformers can be assigned to a bound view for automatic conversion of values, for display and / or input reading.

One of the primary goals of CoconutKit bindings is to be able to define a binding with as few information as possible. The remaining of the binding properties is resolved and checked at runtime. During binding resolution, each bound view is assigned a status. This status can be examined using an in-app debugging interface, making programmer error detection easy.

This article is a hands-on with CoconutKit bindings.

Project setup

Open Xcode 6 or later and create a Single view application project. Add a Podfile next to the .xcodeproj, adding CoconutKit as a dependency:

pod 'CoconutKit', '<version>' 

where version must be at least 3.0. Switch to the command-line and run

pod install

to retrieve all dependencies. Then open the generated workspace.

If you have never used CocoaPods, visit the official website for installation instructions and more information about the Podfile syntax.

Xcode setup

When resolving bindings, key paths are tested for validity, which might lead to NSUnknownKeyException exceptions being thrown. This is especially annoying when you have set an exception breakpoint, but there is fortunately a workaround:

  • Install the LLDB script available here, which makes it possible to ignore exceptions by name or class

  • Edit your exception breakpoint

  • Set exception type to Objective-C

  • Add the following debugger command to ignore NSUnknownKeyException exceptions:

      ignore_specified_objc_exceptions name:NSUnknownKeyException
    

Now the debugger will automatically continue when an NSUnknownKeyException exception is being thrown.

The model

We will create a basic application which lists employees, and provides a way to create new ones. For the sake of simplicity, its design will be kept as minimalist as possible.

To describe an employee, add a new Employee class to your project, with the following implementation:

// ------- Interface -------

#import <Foundation/Foundation.h>

@interface Employee : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSDate *birthdate;

@end

// ---- Implementation -----

@implementation Employee

- (BOOL)validateName:(NSString **)pName error:(NSError **)pError
{
    if (! pName || (*pName).length == 0) {
        if (pError) {
            *pError = [NSError errorWithDomain:@"ch.defagos.BindingsHandsOn"
                                          code:42
                                      userInfo:@{ NSLocalizedDescriptionKey : @"The name is mandatory" }];
        }
        return NO;
    }
    return YES;
}

@end

An employee is simply described by a name and a birthdate. Since an employee must have a name, we implement a corresponding KVC-compliant validation method ensuring this information is not missing. For the sake of simplicity, the returned error is defined using dummy constants. In production applications, you should always cleanly separate your errors into domains with associated error codes using named constants.

Basic navigation

Open the generated storyboard file and remove its contents. Also remove the generated ViewController class from the project. In the following, we will create the following storyboard layout:

Drop a navigation controller onto the storyboard and set it as Initial view controller using the Attributes inspector. Add an EmployeesViewController class, inheriting from UITableViewController, and set it as navigation root view controller class. Its table view will list employees.

In the storyboard, add an Add bar button item on the EmployeesViewController. This button will show a modal view allowing the creation of an employee.

Employee edition

Drop a new view controller onto the storyboard, and bind it to the Add button using a modal segue with the addEmployee identifier. Add two buttons Cancel and Save to this view controller as well.

Draw a second segue from EmployeesViewController to EmployeeEditViewController, with the editEmployee identifier. This segue will be called programatically when tapping on a cell to edit an existing employee, with the cell as sender.

Now add a new class EmployeeEditViewController with the following implementation:

// ------- Interface -------

#import <UIKit/UIKit.h>
#import "Employee.h"

@protocol EmployeeEditViewControllerDelegate;

@interface EmployeeEditViewController : UIViewController <UITextFieldDelegate>

@property (nonatomic, strong) Employee *employee;
@property (nonatomic, weak) id<EmployeeEditViewControllerDelegate> delegate;

@end

@protocol EmployeeEditViewControllerDelegate <NSObject>

- (void)employeeEditViewController:(EmployeeEditViewController *)employeeEditViewController didSaveEmployee:(Employee *)employee;

@end

// ---- Implementation -----

#import "EmployeeEditViewController.h"

@implementation EmployeeEditViewController

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [textField resignFirstResponder];
    return YES;
}

- (IBAction)cancel:(id)sender
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (IBAction)save:(id)sender
{
    [self.delegate employeeEditViewController:self didSaveEmployee:self.employee];
    [self dismissViewControllerAnimated:YES completion:nil];
}

@end

and set it as class for the view controller you just dropped. Bind the two buttons to the associated actions. This view controller does nothing fancy: It lets the user edit an employee, and notifies its delegate when the edited employee needs to be saved. Text fields should register as delegates so that keyboard dismissal is properly handled.

Listing employees

Employees are listed by rows in the table view controller EmployeesViewController. Add an EmployeeTableViewCell class to your project for its cells:

// ------- Interface -------

#import <UIKit/UIKit.h>
#import "Employee.h"

@interface EmployeeTableViewCell : UITableViewCell

@property (nonatomic, strong) Employee *employee;

@end

// ---- Implementation -----

#import "EmployeeTableViewCell.h"

@implementation EmployeeTableViewCell

@end

In the storyboard file, on the EmployeesViewController, select the existing prototype cell and assign it the EmployeeTableViewCell reuse identifier as well as the EmployeeTableViewCell class. Drop two labels onto the cell, one for the employee name, the other for its birthdate.

Now implement the EmployeesViewController class you previously added as follows:

// ------- Interface -------

#import <UIKit/UIKit.h>
#import "EmployeeEditViewController.h"

@interface EmployeesViewController : UITableViewController <EmployeeEditViewControllerDelegate>

@end

// ---- Implementation -----

#import "EmployeesViewController.h"

#import "Employee.h"
#import "EmployeeTableViewCell.h"

@interface EmployeesViewController ()

@property (nonatomic, strong) NSArray *employees;

@end

@implementation EmployeesViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.tableView.rowHeight = 44.f;
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    EmployeeEditViewController *employeeEditViewController = segue.destinationViewController;
    employeeEditViewController.delegate = self;
    
    if ([segue.identifier isEqualToString:@"addEmployee"]) {
        employeeEditViewController.employee = [[Employee alloc] init];
    }
    else if ([segue.identifier isEqualToString:@"editEmployee"]) {
        EmployeeTableViewCell *employeeCell = sender;
        employeeEditViewController.employee = employeeCell.employee;
    }
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.employees.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return [tableView dequeueReusableCellWithIdentifier:@"EmployeeTableViewCell"];
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    EmployeeTableViewCell *employeeCell = (EmployeeTableViewCell *)cell;
    employeeCell.employee = self.employees[indexPath.row];
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    EmployeeTableViewCell *employeeCell = (EmployeeTableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
    [self performSegueWithIdentifier:@"editEmployee" sender:employeeCell];
}

- (void)employeeEditViewController:(EmployeeEditViewController *)employeeEditViewController didSaveEmployee:(Employee *)employee
{
    if (! [self.employees containsObject:employee]) {
        self.employees = self.employees ? [self.employees arrayByAddingObject:employee] : @[employee];
        [self.tableView reloadData];
    }
}

@end

The -prepareForSegue: method creates a fresh Employee instance when the Add button is tapped, before displaying it in the edition view controller. The table view lists the available employees (empty at the beginning), stored in the view controller. When the cell is about to be displayed, the corresponding employee is assigned to the cell employee property. When an new employee is saved, the table view is reloaded to display it. Finally, the user can tap a cell to edit the associated employee.

Try to build and run the application to check that screens work as expected. You can create empty employees, which are then displayed as dummy entries in the table view. You can also edit existing employees by tapping on them.

Now that the application skeleton is in place, we can start binding fields to edit and display employee data.

Bindings for employee edition

In the storyboard file, on EmployeeEditViewController's view, drop a text field and a date picker (displaying only a day, month and year) to edit the employee name and birthdate. Set the view controller as text field delegate.

Thanks to CoconutKit bindings, connecting these controls to the underlying model values is a matter of settings a few parameters in Interface Builder. Binding parameters are available from the Xcode Attributes inspector, and begin with Bind:

The most important attribute to set is Bind Key Path which, as its name suggests, provides the key path a view must be bound to:

  • To bind the text field to the employee name property, select it and set Bind Key Path to employee.name
  • To bind the date date picker to the employee birthdate property, select it and set Bind Key Path to employee.birthdate

Each view is now associated with the corresponding property of the employee made available by EmployeeEditViewController. Bindings are resolved at runtime by climbing up the responder chain, starting from each bound view parent, looking for the first responder for which the key path makes sense. In the present case, lookup will find EmployeeEditViewController and its employee property as perfect match and will bind to it.

This same lookup is performed for other objects involved in bindings, e.g. transformers or delegates (see below). Since a view controller defines a local functional context, all lookups stop at view controller boundaries.

Bindings for the employee list

In the storyboard file, select the first label of the the EmployeeTableViewCell prototype cell, and set Bind Key Path to employee.name. Do the same with the second label, but bind it to employee.birthdate. Again, bindings are resolved by climbing up the responder chain, finding the employee property declared on the cell.

Debugging bindings

Build and run your application. Tap on the Add button, enter a person name and select a birthdate. Then tap on the Save button. The employee should automatically appear in the employee list, thanks to bindings. Its birthdate does not appear, though. How can we find what went wrong?

CoconutKit provides an in-app debugging overlay displaying information about bound views. To display it, pause your application in LLDB, and call:

(lldb) expr (void)[UIView showBindingsDebugOverlay]

before resuming. If you display the overlay while EmployeesViewController is visible, you should get the following result:

Bound views are highlighted with a color corresponding to their status:

  • Views in green are correctly bound
  • Views in red are incorrectly bound, e.g. if the key path is incorrect, or if the value requires transformation first
  • Views in yellow could not be completely checked, e.g. if type compatibility between model and view is yet unknown

By tapping on a view, you get various information about the binding status and the involved objects. More importantly, you can discover the reason why a binding failed.

As expected, the name label is correctly bound while the birthdate label is not. Tap on the red birthdate label to discover that a transformer is missing. Labels namely cannot natively display dates, since there is no standard way to display them as a string. A transformer is therefore required.

Remark

For convenience, you can define an alias for the above debugger command in your .lldbinit file, e.g.:

command alias coconutkit_show_bindings expr {(void)[UIView showBindingsDebugOverlay];}

Transformers

There are several ways to define a transformer. As for key paths, transformers are searched along the responder chain, but also on the model object carrying the property which is bound (here Employee, since we are binding to birthdate). Note that transformers are searched among instance methods, but also among class methods.

Since birthdate display is related to employees and does not depend on the employee, we will add a class method on Employee. Transformer methods must return either an instance of a NSFormatter, NSValueTransformer, or of a class conforming to the HLSTransformer protocol. We here choose to return a date formatter:

+ (NSDateFormatter *)dateFormatter
{
    static NSDateFormatter *dateFormatter;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setDateStyle:NSDateFormatterMediumStyle];
    });
    return dateFormatter;
}

Now open the storyboard file, select the birthdate label, and use the Attributes inspector to assign dateFormatter as Bind transformer of the birthdate label.

Now run the project again: The date is now correctly displayed.

Validation

When you create a person, you can save it without assigning it a name. To trigger the associated KVC-compliant validation method, open the EmployeeEditViewController implementation file, and force bound fields to be validated before an employee can be saved:

- (IBAction)save:(id)sender
{
    if (! [self checkBoundViewHierarchyWithError:NULL]) {
        return;
    }
    
    [self.delegate employeeEditViewController:self didSaveEmployee:self.employee];
    [self dismissViewControllerAnimated:YES completion:nil];
}

Note that you must import CoconutKit.h for the -[UIViewController checkBoundViewHierarchyWithError:] to be available. This method triggers a recursive validation for all bound fields in the view controller's view, and returns YES iff successful. On failure, it returns all validations errors by reference. The returned error information is here ignored by providing NULL as parameter.

If you now try to save a new employee without a name, the view controller will not be dismissed anymore but nothing will happen. How can we tell the user something went wrong? We could of course replace the NULL provided as argument, retrieve the errors from the call and display an alert to the user.

Instead, we will use an additional concept of CoconutKit bindings: The binding delegate. For each bound view, a class conforming to the HLSViewBindingDelegate is searched along the responder chain, starting with the bound view parent. If one is found, it can respond to transformation, validation and update errors. In our case, EmployeeEditViewController defines a functional context (the edition of an employee), it therefore makes sense to make it conform to the HLSViewBindingDelegate protocol (again, you will need to import CoconutKit.h):

@interface EmployeeEditViewController : UIViewController <HLSViewBindingDelegate, UITextFieldDelegate>

// Same as before

@end

Now implement the validation-related failure delegate method:

@implementation EmployeeEditViewController

// Same as before

- (void)boundView:(UIView *)boundView checkDidSucceedWithObject:(id)object
{
    boundView.backgroundColor = [UIColor clearColor];
}

- (void)boundView:(UIView *)boundView checkDidFailWithObject:(id)object error:(NSError *)error
{
    boundView.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.4f];
}

@end

Build and run the application. When no name is entered for an employee, the text field background should turn red.

Wrapping up

This short introduction covered all basics of CoconutKit bindings, from key path resolution, to transformers, validation and binding delegates. There is a lot more you can do with CoconutKit bindings, though. Most UIKit controls support bindings, which means you can display data and gather input in a snap. You can also implement bindings for 3rd party classes, even for your own classes.

For more information, be sure to read the complete header documentation in UIView+HLSViewBinding.h. In particular, the complete documentation discusses binding resolution in detail, which is especially important to understand.

Clone this wiki locally