Archive

ios

In this blog post, I am going to talk about implementing UITableViewController using open-source extension MwfTableViewController that I have implemented.

The posts will be published in multiple parts. Part 1 will cover the basic use case. Part 2 and more (have not been decided) will cover more advanced use cases.

Introduction

UITableViewController is probably one of the most commonly used controller in iOS apps. If you are an iOS developer, chances are you have already used it before.

I am not sure about anyone else, but I find out that it’s easy to repeat same if not similar codes in table view controller implementations. Many of them seems very much like boilerplates. But my concern is that it makes the controller class fat, ugly and very hard to maintain.

I want to put an end to this (or at least improve the situation), so I decided to write an extension to UITableViewController that would make implementing table view more pleasant and maintainable. The objectives are to reduce line-of-codes and improve organization (maintainability) of the codes.

As the time of writing this, MwfTableViewController is at version 0.0.4 and it’s being hosted at Github. If you are using Cocoapods, you can find the Specs here. The features can be summarized as followed:

  • Provide implementation of backing store for table data, at the same time provide better way to updates table content
  • Provide better design to create and configure table row cells
  • Provide improved support for search implementation
  • 100% unit-tested

Throughout this post, I’m going to use reference to Apple’s TableSearch sample codes, and provide the example codes as if it would have been written using MwfTableViewController.

Backing Store for Table Data

Let’s face it, using NSArray of object or NSArray of NSArray of object (table with sections) as table’s backing store for table data is not the best way to do it.

In my opinion, NSArray and UITableView’s datasource suffers some degrees of mismatch on the way it works. It’s most obvious when you need to perform batch updates to UITableView (insertion and deletion of rows). On NSArray, insertion or deletion of object affects the subsequent index of update operation. But when propagating the changes to UITableView, those indexes must be translated to NSIndexPath and/or NSIndexSet by adhering to rules. If you make mistake in translating them, your app would crash, not good!

If you are using NSArray as your backing store, you will always have to repeat this same codes to bridge those mismatch. So why not create backing store that work the same way UITableView datasource works so you can focus on your application logic instead.

That exactly what MwfTableData is for. You can either use this class on your UITableViewController implementation or as part of MwfTableViewController. The following will show example on the latter.

The API

MwfTableData API is grouped in 5 categories: creation, accessing data, inserting data, deleting data and updating data. Check MwfTableViewController.h file to find out about the methods.

Using with MwfTableViewController

Creating and providing initial data

Override method `createAndInitTableData`.

-(MwfTableData *)createAndInitTableData;
{
  MwfTableData * tableData = [MwfTableData createTableData];
  [tableData addRow:[Product productWithType:@"Device" name:@"iPhone"]];
  [tableData addRow:[Product productWithType:@"Device" name:@"iPod"]];
  [tableData addRow:[Product productWithType:@"Device" name:@"iPod touch"]];
  [tableData addRow:[Product productWithType:@"Desktop" name:@"iMac"]];
  [tableData addRow:[Product productWithType:@"Desktop" name:@"Mac Pro"]];
  [tableData addRow:[Product productWithType:@"Portable" name:@"iBook"]];
  [tableData addRow:[Product productWithType:@"Portable" name:@"MacBook"]];
  [tableData addRow:[Product productWithType:@"Portable" name:@"MacBook Pro"]];
  [tableData addRow:[Product productWithType:@"Portable" name:@"PowerBook"]];
  return tableData;
}

Reload Table Data

If your application needs to reload the table with new data, it can be done by setting `tableData` property (you don’t have to call [tableView reloadData]). Example:

-(void)reloadProducts;
{
  MwfTableData * tableData = [MwfTableData createTableData];
  // populate with reloaded data
  self.tableData = tableData; // will reload the table view
}

Batch Updates

A super convenience method `performUpdates:` is provided. Using this method, all the required updates to sync with tableView will be handled by MwfTableViewController. Example:

-(void)loadMoreProducts;
{
  NSArray * moreProducts = …; // load the products
  [self performUpdates:^(MwfTableData * tableData) {
    for (Product * p in moreProducts) {
      [tableData addRow:p];
    }
  }];
}

Before we move on to next section, it’s also worth mentioning that you do not need to implement `numberOfSectionsInTableView:` and `tableView:numberOfRowsInSection:` methods because they are already implemented for you.
Note: The implementation also takes care of scenario when you enable the search function.

Providing Table View Cell

Below shows the typical implementation to provide table view cell.

-(UITableViewCell*)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)ip;
{
  UITableViewCell * cell = nil;
  id item = [listOfItems objectAtIndex:ip.row];
  if ([item isKindOf:[Product class]]) {
    // create and configure cell for Product
  } else if ([item isKindOf:[ProductGroup class]]) {
    // create and configure cell for ProductGroup
  } // … and so on…
  return cell;
}

So, What’s the problem with the example above? Well, as I said earier it’s ugly, really ugly. What worse is it’s really hard to maintain.

To solve this, MwfTableViewController takes charge of implementing the `tableView:cellForRowAtIndexPath:` method.
What you need to do is implement the creation and configuration method for each of the data types (e.g. Product and ProductGroup) by following very simple naming convention.

Creation Method

`tableView:cellFor<DataClassName>AtIndexPath:`
or you can use short form `$cellFor(<DataClassName>)`

Configuration Method

`tableView:configCell:for<DataClassName>:`
or you can use short form `$configCell(<DataClassName>,<CellClassName>)`

To use the `$` short forms, you must define `CK_SHORTHAND` before importing `MwfTableViewController`.

#define CK_SHORTHAND
#import "MwfTableViewController.h"

Example:

-$cellFor(Product);
{
  static NSString * CellIdentifier = @"ProductCell";
  UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
  if (!cell) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
  }
  return cell;
}
-$configCell(Product,UITableViewCell);
{
  cell.textLabel.text = product.name;
}

That’s it, no more writing the endless and ugly if-else.
You can further improve your codes organization by implementing those methods in a objective-c category. By doing that, you keep your controller class lean and write the creation and configuration method for each data type ONCE.
Example:

@interface MwfTableViewController (TableSearchCells)
-$cellFor(Product);
-$configCell(Product,UITableViewCell);
-$cellFor(ProductGroup);
-$configCell(ProductGroup,ProductGroupCustomCell);
// … and so on
@end

Implementing Search

Using MwfTableViewController to implement search is dead simple. You basically just need to do 2 things:

Enable Search

By setting `wantSearch` property to YES.

-(id) initWithStyle:(UITableViewStyle)style;
{
  self = [super initWithStyle:style];
  if (self) {
    self.wantSearch = YES;
  }
  return self;
}

Override the create search results method

The method name is `createSearchResultsTableDataForSearchText:scope:`.

-(MwfTableData *)createSearchResultsTableDataForSearchText:(NSString *)searchText scope:(NSString *)scope;
{
  MwfTableData * resultData = [MwfTableData createTableData];
  NSUInteger count = self.tableData.numberOfRows;
  for (int i = 0; i < count; i++) {
    Product * product = [self.tableData objectForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
    NSComparisonResult result = [product.name compare:searchText options:(NSCaseInsensitiveSearch|NSDiacriticInsensitiveSearch) range:NSMakeRange(0, [searchText length])];
    if (result == NSOrderedSame) {
      [resultData addRow:product];
    }
  }
  return resultData;
}

So that’s it. That’s all you need to do to implement basic table view with search function using MwfTableViewController.

If you would compare, it actually cuts around 50% of codes you need to write. Less codes means less bugs.
And you also get more elegant codes that is easier to maintain.

Finally, thanks for reading this post. Please share if you like it. If you have questions or feedbacks, you can post them in comments.

Advertisements

The first version of slide navigation controller was published 3 weeks ago. Few changes are made since then.

Deprecated Method

slideForViewController:direction:portraitOrientationDistance:landscapeOrientationDistance: method has been deprecated, although it’s still around to maintain the backward-compatibility.

The new version of the API require user to implement a new MWFSlideNavigationViewControllerDataSource protocol described below. To trigger the slide navigation, use the new method slideWithDirection:

New MWFSlideNavigationViewControllerDataSource protocol

This protocol has only 1 method to be implemented by application, i.e.

– (UIViewController *) slideNavigationViewController:(MWFSlideNavigationViewController *)controller viewControllerForSlideDirecton:(MWFSlideDirection)direction

Implementor should return instance of view controller to be shown for the specified slide direction.

Experimental Pan/Swipe Gesture

In this version, pan/swipe gesture is supported by setting panEnabled property to YES (default is NO).

If you have any feedback or suggestions for improvements, kindly leave them in comments.

Building an app with slide navigation to reveal a hidden view/menu beneath the main view, like one seen in Facebook and Piggie (picture of the left) ?

Well, that is exactly the purpose of the slide navigation view controller discussed in this post.

Why a view hidden beneath the main view?

I think there are at least 2 reasons for this design.

  • Reserve valuable screen real estate, this is true especially for the iPhone. With very limited space available, you want to keep only the important things front and center.
  • Cleaner/Uncluttered look and feel. For the sample apps mentioned above, It will easily clutter the screen when the ‘hidden’ view that contains menus/buttons are constantly shown.

MWFSlideNavigationViewController

This component is designed to be simple and easy to use. You call initWithRootViewController: method to initialize with a root UIViewController object. The root view controller is the main view of your application.

Subsequently, you can call slideForViewController:direction:portraitOrientationDistance:landscapeOrientationDistance: to reveal the secondary hidden view. The same method is used to slide the main view back.

This method takes 4 parameters:

  •  viewController, this is the view controller that manages the secondary hidden view. If you call this method to slide the main view back, you can supply nil value for the view controller.
  • direction, i.e. MWFSlideDirectionUp, MWFSlideDirectionLeft, MWFSlideDirectionDown, MWFSlideDirectionRight. To slide main view back, use MFWSlideDirectionNone.
  • portraitOrientationDistance, the slide distance in pixels when in portrait orientation.
  • landscapeOrientationDistance, the slide distance in pixels when in landscape orientation.

For custom/application specific behavior on the event when the sliding will/did occur, your can implement the MWFSlideNavigationViewControllerDelegate protocol. Example usage of the protocol is demonstrated in the Demo app, to show overlay view with tap gesture recognizer on the main view. When the overlay view is tapped, it slides the main view back to conceal the secondary view.

Additionally, there is a category included to provide convenient access to the slide navigation view controller to your view controller. Just like the built-in navigationController property in UIViewController.

How to use in your project

Download/fork the source code from Github, then add the MWFSlideNavigationController .h and .m files into your project.

Requirements

  • iOS 5 and above only
  • Full ARC

Source Codes and Documentation

The source codes are hosted at github.
You can refer to the documentation here.

Licensing

This is free and open source component covered under the MIT license. You are free to use and modify the source as long as you retain the copyrights and include permission notice in all copies/substantial portions of your software.

For attribution free license, you can contact me at meiwin@blockthirty.com.

Hope this component helps in your project. You can consider paypal donation to venansius.meiwin@gmail.com if you like what you see.

 

Updates: new post describing changes introduced in version 0.2

%d bloggers like this: