Wednesday, April 10, 2013

iOS 6 Pull-to-Refresh

A new feature in iOS 6, the pull-to-refresh control available for UITableViewControllers.
Some things to keep in mind with this new control are the following:
  • It’s only available for UITableViewControllers, not regular UITableView instances
  • Cannot customize the shape or design, only the tint color
  • Cannot be added via Interface Builder, only programmatically
These may change at some point in the future but for now you’ll have to work with them.
The best way to showcase this new control is to jump straight into Xcode and start a new project. Please do this and select the Master-Detail Application iOS template.

Click Next and use Refresher as the Product Name, enter your Organization Name and Company Identifier, make sure only iPhone is selected for Devices and only check the Automatic Reference Counting and Storyboards checkboxes.


Click Next one more time and select where to save the project.
The template is pretty standard, you have a storyboard file for iPhone with a Navigation Controller, a UITableViewController as its root controller and a UIViewController for when a cell is tapped in the table. You will also notice the appropriate subclasses for the table view controller and the view controller.
Open MasterViewController.m and delete all of the method implementations except for the following:
    • didReceiveMemoryWarning
    • numberOfSectionsInTableView:
    • tableView:numberOfRowsInSection:
    • tableView:cellForRowAtIndexPath:
    • prepareForSegue:sender: 
After you do this, go ahead and delete the _objects variable declared in the class extension and replace it with this property declaration:
 @interface MasterViewController ()  
 @property (strong, nonatomic) NSArray *objects;  
 @end  

You’re not going to let users add or remove items to the table in this project, only use the pull-to-refresh control to invert the sorting of the items. It’s for this reason that a property with a custom getter method will be easier to work with. Add this code inside your implementation block:

- (NSArray *)objects  
 {  
    if (!_objects)  
    {  
      _objects = @[@"Argentina", @"Australia", @"Brazil", @"Ecuador", @"England", @"Germany", @"Italy", @"Japan", @"New Zealand", @"United States"];  
    }  
    return _objects;  
 }  

This initializes the objects array with the names of some countries ordered from A-Z. Before running the project you need to make a few changes in order to use this property. Update the following methods as shown:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section  
 {  
    return self.objects.count;  
 } 
  - (UITableViewCell *)tableView:(UITableView *)tableView  cellForRowAtIndexPath:(NSIndexPath *)indexPath  
 {  
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];  
    cell.textLabel.text = self.objects[indexPath.row];  
    return cell;  
 }  
 - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender  
 {  
    if ([[segue identifier] isEqualToString:@”showDetail”])  
    {  
      NSIndexPath *indexPath = [self.tableView  indexPathForSelectedRow];  
      [[segue destinationViewController] setDetailItem:self.objects[indexPath.row]];  
    }  
 }  

All you’re doing here is using the property instead of the instance variable. This will, if necessary, create the objects array with the preloaded list of countries.
Go ahead and run the project on a device or the simulator and check out the results:

Sweet, the table view is looking just as expected albeit without any pull-to-refresh. How about tackling that next?
Go back to MasterViewController.m and add the following implementation for viewDidLoad:
- (void)viewDidLoad  
 {  
    [super viewDidLoad];  
    UIRefreshControl *refreshControl = [[UIRefreshControl alloc]  init];  
    refreshControl.tintColor = [UIColor magentaColor];  
    self.refreshControl = refreshControl;  
 }  

As mentioned before, the UIRefreshControl has to be created programmatically. You do the standard alloc and init, set the tint color (this is optional but I wanted to show you that it can be done) and assign it to the table view controller’s refreshControl property.
Run the project one more time and pull the table view down:
Things are looking good. But, umm, the activity indicator never really stops!
This is because you need to respond to the valueChanged control event on the refresh control. To do this, update viewDidLoad as follows:
 
- (void)viewDidLoad  
 {  
  [super viewDidLoad];  
  UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];  
  refreshControl.tintColor = [UIColor magentaColor];  
  [refreshControl addTarget:self action:@selector(changeSorting) forControlEvents:UIControlEventValueChanged];  
   self.refreshControl = refreshControl;  
 } 

And to avoid a runtime crash when you pull-to-refresh, stub out the changeSorting method:

 - (void)changeSorting  
 {  
 } 

Once again run your project. Notice how you are still getting the endless activity indicator. To fix this, add the following property declaration in your class extension:

  @property (assign, nonatomic) BOOL ascending;  

And add this code to changeSorting:

- (void)changeSorting  
 {  
   NSSortDescriptor *sortDescriptor = [[NSSortDescriptoralloc] initWithKey:nil ascending:self.ascending];  
   NSArray *sortDescriptors = @[sortDescriptor];  
   _objects = [_objects sortedArrayUsingDescriptors:sortDescriptors];  
   _ascending = !_ascending;  
   [self performSelector:@selector(updateTable) withObject:nil afterDelay:1];  
 }  

This code uses a sort descriptor to change the sort order of the array by using the BOOL property created a second ago. You also call a method named updateTable with a small delay that will handle reloading the table data and setting the refresh control to stop updating.
Add the implementation for that private method as shown:
 - (void)updateTable  
 {  
    [self.tableView reloadData];  
    [self.refreshControl endRefreshing];  
 } 

Go ahead and run the project one more time, pull the table view down and notice how the list of countries is now using the reverse sort order!