Lighter View Controller Data Sources

Inspired by the concept of Lighter View Controllers, neatly explained by Chris Eidhof on objc.io, I came up with a way of separating the view controller, data source and network code.

If you haven’t read the aforementioned article I suggest you do it and then come back to this page.

I started out with an ArrayDataSource like Chris suggested:

self.dataSource = [[ArrayDataSource alloc] initWithItems:photos
                               cellIdentifier:CellIdentifier
                               configureCellBlock:configureCell];

tableView.dataSource = self.dataSource;

My experience using ArrayDataSource #

After some time three issues popped up:

  1. Most of my view controllers had collection/table views that present results from a web service call. ArrayDataSource only supports arrays, so I had to fetch and parse from the server before creating the data source.
  2. I needed pagination. That means my datasource should be able to grow.
  3. I needed some modification features: move, replace, delete, etc…

I could easily handle the first issue by creating the data source after the network call finished. The view controller would be the usual place for that code, but I wanted to keep it light.

The second issue could be handled by replacing the array with a bigger one each time I had to add a page.

The last was the trickiest. I was starting to have a lot of code outside the data source only to mutate it. That code should go inside the data source.

Enter RemoteDataSource #

@interface RemoteDataSource : NSObject

- (instancetype)initWithServiceURL:(NSURL *)serviceURL
                  cellIdentifier:cellIdentifier 
                  configureCellBlock:configureCellBlock 
                  pageParameterName:(NSString *)pageParameterName 
                  parser:(id<RemoteDataSourceParserProtocol>)parser;

- (void)fetchWithCompletionBlock:(void (^)(NSError *error))block;

- (BOOL)fetchMoreWithCompletionBlock:(void (^)(NSError *error, NSArray *indexPaths))block;

- (void)cancel;

@end
id<RemoteDataSourceParser> parser = ...

self.dataSource = [[RemoteDataSource alloc] initWithServiceURL:url 
                                            cellIdentifier:CellIdentifier 
                                    configureCellBlock:configureCellBlock
                                            pageParameterName:@"page" 
                                            parser:aParser];

tableView.dataSource = self.dataSource;

As you can see I decided to separate the parsing code from the fetching code. The idea is that you might have different service results for each calls. For example, one for shops, one for locations, etc…

The parser protocol is also pretty straightforward:

@protocol RemoteDataSourceParserProtocol

- (NSArray *)parseData:(NSData *)data error:(NSError **)error;

@end

As you can see, easy as pie to implement. Here’s an example:


@implementation ShopsRemoteParser

- (NSArray *)parseData:(NSData *)data error:(NSError **)error pageValue:(inout NSUInteger *)pageValue {
    NSError *err;

    id o = [NSJSONSerialization JSONObjectWithData:data options:0 error:&err];

    if (err) {
        if (error) *error = err;
        return nil;
    }

    if (![o isKindOfClass:[NSArray class]]) {
        if (error) *error = [NSError errorWithDomain:@"RemoteDataSourceParsingErrorDomain" code:0 userInfo:nil];
        return nil;
    }

    NSMutableArray *arr = [NSMutableArray arrayWithCapacity:[o count]];

    for (id dict in o) {
        if (![dict isKindOfClass:[NSDictionary class]]) continue;
        id obj = [[Shop alloc] initWithInfo:dict];
        if (obj) [arr addObject:obj];
    }

    return [arr copy];
}

@end

What should a data source do? #

As the name implies, a data source should provide data to other object, frequently a view or view controller. So should it know how to create views? No it shouldn’t. That’s why Chris added that configureCellBlock.

Just a quick note. I would love to know the reason behind tableView:cellForRowAtIndexPath:. I mean, why did Apple decided it should be the data source’s job to create cells?

Let’s imagine you have a collection view of Flickr photos, as you scroll down the next page of items is appended to the bottom. When you tap one item you present a new view controller with a full screen collection view that shows the same photos. As you (horizontally) scroll to the end of this collection view the next page should be appended to the right.

You want both collection views to share the same data source. It’s the same data. And you want to reflect the same state on both as the user goes back and forth. But if the data source owns the cell identifier and configuration block you cannot share it between collection views. The cells are completely different.

A lighter RemoteDataSource #

To be able to pass data sources around they can’t have any cell configuration code. So I refactored it into UIDataSource and RemoteDataSource. And while at it I also introduced AbstractDataSource.

AbstractDataSource
     RemoteDataSource
     ArrayDataSource
UIDataSource

UIDataSource conforms to UITableViewDataSource and UICollectionViewDataSource.

@interface UIDataSource : NSObject <UICollectionViewDataSource, UITableViewDataSource>

@property(nonatomic, strong) AbstractDataSource *dataSource;
@property(nonatomic, copy, readonly) NSString *cellIdentifier;

- (instancetype)initWithDataSource:(AbstractDataSource *)dataSource cellIdentifier:(NSString *)cellIdentifier configureCellBlock:(void (^)(id cell, id object, NSIndexPath *indexPath))block;

@end

Now each view controller can have a UIDataSource and the inner AbstractDataSource can be shared.

Note: This concept came to me after reading how NSTableViewDataSource works. Basically the data source only provides an object. It’s the cell’s job to display it.

Mutating the data source #

The AbstractDataSource class is actually just a definition of the needed methods and a placeholder implementation. A bunch of your typical NSAssert(@"implement this on a subclass"); methods.

It has methods for adding/removing/moving/replacing items, all with empty implementations.

@interface AbstractDataSource : NSObject <UICollectionViewDataSource, UITableViewDataSource>

- (id)objectForKeyedSubscript:(NSIndexPath *)indexPath;
- (NSIndexPath *)indexPathForObject:(id)object;
- (NSUInteger)count;

- (void)insertObject:(id)object atIndexPath:(NSIndexPath *)indexPath;
- (BOOL)deleteItemAtIndexPath:(NSIndexPath *)indexPath;
- (BOOL)moveItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath;
- (void)replaceObjectAtIndexPath:(NSIndexPath *)indexPath withObject:(id)object;

@end

One last thing you might notice is that AbstractDataSource still conforms to UICollectionViewDataSource and UITableViewDataSource. I figured that the data source needs to be able to define the number of sections and rows. If you have a DictionaryDataSource with { title : array } pairs for example.

It doesn’t make sense for a RemoteDataSource to be modified, and the code to remove an item from ArrayDataSource will certainly be different from SQLTableDataSource.

The final setup #

The way I described those classes is a simplification of the actual code I ended up with, but I think it’s enough to explain the concept. You can now add caching, errors, new data sources, etc…

This is just an idea. Everybody has a way of organizing code. This happens to be mine at the moment. The best one I found so far for these types of applications.

You can find a sample project on Github.

I can’t recommend objc.io enough by the way.

 
11
Kudos
 
11
Kudos

Now read this

Sharing Core Data between App and Extension

For years now I’ve been avoiding Core Data. My first experience with it was back when the iPad first came out and it wasn’t pleasant. I was able to ignore it for so long because most of the projects I worked on were just client apps for... Continue →