Building an SDK: Part II

This is the second installation of Building an SDK. A series of posts about the trials and tribulations of creating and shipping the Marqeta iOS SDK. If you haven't read the first part, please check it out here.

Part II: Design Patterns

A common occurence in iOS development is the use of the MVC design pattern. Apple defines MVC as:

The Model-View-Controller (MVC) design pattern assigns objects in an application one of three roles: model, view, or controller. The pattern defines not only the roles objects play in the application, it defines the way objects communicate with each other.

But most recently, well-experienced Cocoa developers have comically resorted to Massive View Controller as the true definition of this pattern, which brings me to the topic of this post—What is the best way to create a modularized SDK that would allow developers to use a variety of design patterns to keep their view controllers light (like MVVM), while still allowing those who are comfortable with MVC to use it as they wish?

There are a few reasons why I'd like to internally implement lighter view controllers, but unit testing is definitely a front-runner! I'll get to that in the next part of this series. But the second-most important enhancement from the MVC pattern is the DataSource Pattern. Everyone should be familiar with the implementation of a UITableViewController/UICollectionViewController, there are so many required delegate and dataSource methods that must be implemented for the app to run successfully. For example:

UITableViewDataSource:
UICollectionViewDataSource:

For a moderately-sized iOS application utilizing one or more of these views, these pairs of protocol methods can be very large, complex, and hard to maintain. The logic within these methods are usually clearly defined for a particular use case and cannot be reused elsewhere. Lastly, with MVC, the view controller that contains one of these views has to determine the class of each cell, as well as it's backed data, and assign the proper properties each time the cell is displayed.

I would like to separate a UITableView's/UICollectionView's dataSource and delegate handling from the parent view controller that contains them. The view controller doesn't need to know about the dataSource or delegate objects themselves, just that it owns a table view or collection view object and conforms to their given protocols.

At the end of the day, what this does is creates an abstraction between the types of cells/items these views have and allows for decoupling of dependencies.


The Solution:

Build a subclass of NSObject (e.g., MQTableViewDelegateAndDataSource or MQCollectionViewDelegateAndDataSource) that is used to handle these required (and some optional) protocol methods. This way, the parent view controller can own the table view and/or collection view, and also our custom dataSource object.

Cell For Object Protocol:

Here we are making a protocol to construct and return a cell of the type UITableViewCell, UICollectionViewCell, or any subclass of the two. The majority of this model is to abstract the cell creation and configuration methods, which tend to contain a lot of logic and can inflate the view controller heavily.

UITableView:
@protocol MQTableViewCellForObjectDelegate <NSObject>
@optional

// Delegate for configuring a UITableViewCell:
- (id<MQBindingDataForObjectDelegate>)cellForItemForTableView:(UITableView *)tableView;
@interface MQUserObject (MQTableViewCellForObject) <MQTableViewCellForObjectDelegate>
- (UITableViewCell *)cellForObjectForTableView:(UITableView *)tableView;
@end

@implementation MQUserObject (MQTableViewCellForObject)

- (id<MQBindingDataForObjectDelegate>)cellForObjectForTableView:(UITableView *)tableView
{
    id<MQBindingDataForObjectDelegate> cell = [tableView dequeueReusableCellWithIdentifier:@"cellIdentifier"];
    return cell;
}

@end
UICollectionView:
@protocol MQCollectionViewCellForObjectDelegate <NSObject>
@optional

// Delegate for configuring a UICollectionViewCell:
-(id<MQBindingDataForObjectDelegate>)cellForItemForCollectionView:(UICollectionView *)collectionView;
@end
@interface MQUserObject (MQCollectionViewCellForObject) <MQCollectionViewCellForObjectDelegate>
- (UICollectionViewCell *)cellForObjectForCollectionView:(UICollectionView *)collectionView;
@end

@implementation MQUserObject (MQCollectionViewCellForObject)

- (id<MQBindingDataForObjectDelegate>) cellForObjectForCollectionView:(UICollectionView *)collectionView 
{
    id<MQBindingDataForObjectDelegate> cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cellIdentifier" forIndexPath:indexPath];
    return cell;
}

@end

Note: In the previous examples, I have separated each view type and their given delegate and dataSource methods into their own protocol objects. However, you can combine these methods into one object by making all of the methods @optional and then create two separate extensions for each type, which you would then override the methods to be @required. For example:

// The generic "Cell For Object" protocol, with all methods set to `@optional`
@protocol MQCellForObjectDelegate <NSObject>
    @optional

    // MQCollectionViewCellForObjectDelegate:
    -(id<MQBindingDataForObjectDelegate>)cellForItemForCollectionView:(UICollectionView *)collectionView;

    // MQTableViewCellForObjectDelegate:
    -(id<MQBindingDataForObjectDelegate>)cellForItemForTableView:(UITableView *)tableView;

    /* ... */
@end

// The protocol extension to set the UICollectionView methods to `@required`
@protocol MQCollectionViewCellForObjectDelegate <MQCellForObjectDelegate>
// @required
// Delegate for configuring a UICollectionViewCell:
-(id<MQBindingDataForObjectDelegate>)cellForItemForCollectionView:(UICollectionView *)collectionView;
@end

// The protocol extension to set the UITableView methods to `@required`
@protocol MQTableViewCellForObjectDelegate <MQCellForObjectDelegate>
// @required
// Delegate for configuring a UITableViewCell:
-(id<MQBindingDataForObjectDelegate>)cellForItemForTableView:(UITableView *)tableView;
@end

Binding Data Protocol:

Here we are making a protocol to bind the data from every cell of the type UITableViewCell, UICollectionViewCell, or any subclass of the two. This should enable the cell's themselved to configure their own subviews. A slight deviation of a ViewModel from the MVVM design pattern.

@protocol MQBindingDataForObjectDelegate<NSObject>
- (void)bindingDataForObject:(NSObject *)object;
@end
UITableViewCell:
@interface MQUserTableViewCell (MQBindingData) <MQBindingDataForObjectDelegate>
- (void)bindingDataForObject:(NSObject *)object;
@end

@implementation MQUserTableViewCell (MQBindingData)

- (void)bindingDataForObject:(NSObject *)object
{
    MQUser *user = (MQUser *)object;
    [self.textLabel setText:user.name];
    [self.imageView setImage:user.image];
}

@end
UICollectionViewCell:
@interface MQUserCollectionViewCell (MQBindingData) <MQBindingDataForObjectDelegate>
- (void)bindingDataForObject:(NSObject *)object;
@end

@implementation MQUserCollectionViewCell(MQBindingData)

- (void)bindingDataForObject:(NSObject *)object
{
    MQUser *user = (MQUser *)object;
    [self.textLabel setText:user.name];
    [self.imageView setImage:user.image];
}

@end

DelegateAndDataSource Protocol:

Abstract the UITableView or UICollectionView delegate and dataSource to it's own class.

UIViewController with a UITableView:
@interface MQTableViewDelegateAndDataSource : NSObject <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, copy) void(^tableViewCellDidSelectBlock)(id object);
- (instancetype)initWithData:(NSArray *)data NS_DESIGNATED_INITIALIZER;
@end
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    id<MQCellForObjectDelegate> object = [self.data objectAtIndex:indexPath.row];
    id<MQBindingDataForObjectDelegate> cell = [object cellForObjectForTableView:tableView];
    [cell bindingDataForObject:object];
    return (UITableViewCell *)cell;
}
@interface MQUserTableViewController ()
@property (nonatomic, strong) MQTableViewDelegateAndDataSource *dataSource;
@end

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Other setup ...

    self.dataSource = [[MQTableViewDelegateAndDataSource alloc] initWithData:array];
    self.tableView.delegate = self.dataSource;
    self.tableView.dataSource = self.dataSource;
    self.dataSource.tableViewCellDidSelectBlock = ^(id object){
        NSLog(@"Selected: %@", [object name]);
    };
}
UIViewController with a UICollectionView:
@interface MQCollectionViewDelegateAndDataSource : NSObject <UICollectionViewDelegate, UICollectionViewDataSource>
@property (nonatomic, copy) void(^collectionViewCellDidSelectBlock)(id object);
- (instancetype)initWithData:(NSArray *)data NS_DESIGNATED_INITIALIZER;
@end
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    id<MQCellForObjectDelegate> object = [self.data objectAtIndex:indexPath.row];
    id<MQBindingDataForObjectDelegate> cell = [object cellForObjectForTableView:tableView];
    [cell bindingDataForObject:object];
    return (UICollectionViewCell *)cell;
}
@interface MQUserCollectionViewController ()
@property (nonatomic, strong) MQCollectionViewDelegateAndDataSource *dataSource;
@end

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Other setup ...

    self.dataSource = [[MQCollectionViewDelegateAndDataSource alloc] initWithData:array];
    self.collectionView.delegate = self.dataSource;
    self.collectionView.dataSource = self.dataSource;
    self.dataSource.collectionViewCellDidSelectBlock = ^(id object){
        NSLog(@"Selected: %@", [object name]);
    };
}

What We Learned:

This pattern was essential for breaking apart all of that bloated boilerplate code that Apple forces us all to use. We were able to centralize the logic surrounded how a UITableView or a UICollectionView is configured and removed the backing data from the view controller's implementation.

After moving to this code structure, we also realized how much easier it was to unit test our collections. We could not keep our view controller tests minimal and focus on the handling of our cell selection states rather than the construction of each cell.

This also allowed us to test every instance of our custom protocols to ensure that they perform as expected globally—which means no more individual tweaks! I tend to believe that any pattern than makes it easy and more enjoyable to test your code should be considered a gift and utilized as much as possible.

Comments