Skip to content
This repository

A subclassable table view controller with empty, loading and error states, also supports infinite scrolling and pull to refresh.

branch: master
README.md

JMStatefulTableViewController

This is the class I use whenever I need to implement a "stateful" table view in an iOS app. In this context, when I say "stateful" I mean a table view controller that has the following "states":

  • Initially loading for the first time since instantiating. (Usually displaying a "loading" view covering the table view entirely).
  • An "idle" state, where the user can scroll around and consume content, no special activity happening.
  • Loading from a "pull to refresh" gesture.
  • Loading the next "page" in a scenario where I need to scroll "infinitely."
  • Empty (Usually displaying a nice looking "empty" view covering the table view entirely).
  • Error (This is useful when the "initial" load fails or I need to communicate that some other horrible thing has happened).

If you're using JMStatefulTableViewController in your application, add it to the list.

Screenshots

   

Example Usage

The demo project hosted in this repo is the first place you should look for how to implement JMStatefulTableViewController in your app, but basically you just need to subclass JMStatefulTableViewController and implement the required delegate methods on that subclass.

The next section shows an example of how you might implement the required delegate methods.

First Time Loading

JMStatefulTableViewController will call it's statefulDelegate with this method, passing it in two blocks, a success and failure block, when the table view needs to load it's "initial" bit of content. It will also transparently handle changing the state to JMStatefulTableViewControllerStateInitialLoading for you.

You should write or call your code to load your initial set of content inside this method, and then call the correct block for the outcome. If your data loaded successfully, call the success, if it failed for some reason call the failure block, optionally passing in an NSError object, or nil.

- (void) statefulTableViewControllerWillBeginInitialLoading:(JMStatefulTableViewController *)vc completionBlock:(void (^)())success failure:(void (^)(NSError *error))failure {
    // Always do any sort of heavy loading work on a background queue:
    dispatch_async(dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_DEFAULT), ^{
        self.catPhotos = [self _loadHilariousCatPhotosFromTheInternet];

        // Always call success() on the main queue:
        dispatch_async(dispatch_get_main_queue(), ^{
            success();
        });
    });
}

Loading From Pull To Refresh

JMStatefulTableViewController will call it's statefulDelegate with this method, passing it in two blocks, a success and failure block when the user finishes a "pull to refresh" gesture. Note that the success block in this case is asking for an array of NSIndexPath objects.

I've implemented it this way so I can easily achieve what I call "proper" pull to refresh style loading. In "proper" pull to refresh loading, the existing content stays in place and the new content appears above it, without offsetting the table view at all. This is how Loren Brichter (original inventor of the concept) originally invented and intended it to work. In my opinion it also makes more logical sense. However, if you'd like, you can simple pass nil in for the array of NSIndexPaths or an empty NSArray object, and JMStatefulTableViewController will degrade gracefully, replacing the content in your tableview with the latest content.

You should write or call your code to load any newer content than the current first item (or optionally just reload everything, like many apps do these days), and then call the correct block for the outcome. If your data loaded successfully, call the success block, if it failed for some reason call the failure block, optionally passing in an NSError object, or nil.

- (void) statefulTableViewControllerWillBeginLoadingFromPullToRefresh:(JMStatefulTableViewController *)vc completionBlock:(void (^)(NSArray *indexPathsToInsert))success failure:(void (^)(NSError *error))failure {
    // Always do any sort of heavy loading work on a background queue:
    dispatch_async(dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_DEFAULT), ^{
        // Grab what is currently our first photo
        CatPhoto *photo = [self.catPhotos objectAtIndex:0];

        // Load any newer photos that might have been added on our server
        NSArray *catPhotos = [self _loadHilariousCatPhotosFromTheInternetNewerThanPhoto:photo];

        // Prepend our self.catPhotos array with these new photos we loaded
        self.catPhotos = [catPhotos arrayByAddingObjectsFromArray:self.catPhotos];

        // Put together an array of NSIndexPath objects representing
        // what the index paths will be of the new rows that will be created
        NSMutableArray *a = [NSMutableArray array];

        for(NSInteger i = 0; i < loadedBeerStrings.count; i++) {
            [a addObject:[NSIndexPath indexPathForRow:i inSection:0]];
        }

        // Always call success() on the main queue:
        dispatch_async(dispatch_get_main_queue(), ^{
            // If we didn't want to achieve "proper" pull to refresh behavior, we could just pass `nil` in here:
            success([NSArray arrayWithArray:a]);
        });
    });
}

Loading The Next "Page"

JMStatefulTableViewController will call it's statefulDelegate with this method, passing it in two blocks, a success and failure block, when the users scrolls to the bottom of your table view.

You should write or call your code to load the next set of content, and then call the correct block for the outcome. If your data loaded successfully, call the success block, if it failed for some reason call the failure block, optionally passing in an NSError object, or nil.

- (void) statefulTableViewControllerWillBeginLoadingNextPage:(JMStatefulTableViewController *)vc completionBlock:(void (^)())success failure:(void (^)(NSError *))failure {
    // Always do any sort of heavy loading work on a background queue:
    dispatch_async(dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_DEFAULT), ^{
        // Grab what is currently our last photo
        CatPhoto *photo = [self.catPhotos lastObject];

        // Load any older cat photos from our server
        NSArray *catPhotos = [self _loadHilariousCatPhotosFromTheInternetOlderThanPhoto:photo];

        // Append the new photos we've loaded to the end of your self.catPhotos array
        self.catPhotos = [self.catPhotos arrayByAddingObjectsFromArray:catPhotos];

        // Always call success() on the main queue:
        dispatch_async(dispatch_get_main_queue(), ^{
            success();
        });
    });    
}   

Loading The Next "Page"

JMStatefulTableViewController will call it's statefulDelegate with this method to determine if it can load any more content.

You should return a value indicating whether or not any more content exists to be loaded. This will control whether or not the user is shown a "Loading more" visual state.

- (BOOL) statefulTableViewControllerShouldBeginLoadingNextPage:(JMStatefulTableViewController *)vc {
    return [self _areThereAnyMoreHilariousCatPhotosOnTheServer];
}

Pull To Refresh Customization

JMStatefulTableViewController uses @samvermette's excellent SVPullToRefresh library to accomplish both pull to refresh and infinite scrolling. It is very customizable, you can read all about how in SVPullToRefresh's documentation.

Empty, Loading and Error Views

The demo app in this repo uses the built-in implementations of these views. Right now, they are simply full width and height solid color views, to give you something to look at when building your app.

You can subclass JMStatefulTableViewLoadingView, JMStatefulTableViewEmptyView and JMStatefulTableViewErrorView respectively. Currently, they do not offer any special functionality or look and feel, but in the future they will emulate a "system" look and feel for these states. Feel free to take them or leave them.

JMStatefulTableViewController has three properties:

@property (strong, nonatomic) UIView *emptyView;
@property (strong, nonatomic) UIView *loadingView;
@property (strong, nonatomic) UIView *errorView;

You can set these to any UIView you'd like, to indicate any of these states. Like I said, right now, by default, they're not anything useful, just solid colored views.

Adding To Your Project

With CocoaPods

If you are using CocoaPods then just add this line to your Podfile:

pod 'JMStatefulTableViewController'

Now run pod install to install the dependency.

Without CocoaPods

Download the source files or add it as a git submodule. Here's how to add it as a submodule:

$ cd YourProject
$ git submodule add https://github.com/jakemarsh/JMStatefulTableViewController.git Vendor/JMStatefulTableViewController

Add all of the Objective-C files to your project.

If you're installing this way, (instead of using CocoaPods) you'll also need to separately install SVPullToRefresh on your own, as described in the SVPullToRefresh README. (For this reason, and because it's an awesome system, I strongly reccomend using CocoaPods).

JMStatefulTableViewController uses Automatic Reference Counting (ARC). If your project doesn't use ARC, you will need to set the -fobjc-arc compiler flag on all of the JMStatefulTableViewController source files. To do this in Xcode, go to your active target and select the "Build Phases" tab. In the "Compiler Flags" column, set -fobjc-arc for each of the JMStatefulTableViewController source files.

Something went wrong with that request. Please try again.