Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial migration of codebase out of RestKit

* Updated to build against RestKit 0.20.0-dev
* Converted to ARC
* Added CocoaPod for installation
  • Loading branch information...
commit efe00bf0387efa1f9e90aa7f01b9d86399abc143 1 parent 70dced1
Blake Watters blakewatters authored
Showing with 5,932 additions and 0 deletions.
  1. +49 −0 Code/NSBundle+RKTableControllerAdditions.h
  2. +53 −0 Code/NSBundle+RKTableControllerAdditions.m
  3. +444 −0 Code/RKAbstractTableController.h
  4. +1,338 −0 Code/RKAbstractTableController.m
  5. +79 −0 Code/RKAbstractTableController_Internals.h
  6. +68 −0 Code/RKFetchedResultsTableController.h
  7. +675 −0 Code/RKFetchedResultsTableController.m
  8. +57 −0 Code/RKKeyboardScroller.h
  9. +144 −0 Code/RKKeyboardScroller.m
  10. +45 −0 Code/RKMutableBlockDictionary.h
  11. +136 −0 Code/RKMutableBlockDictionary.m
  12. +60 −0 Code/RKObjectManager+RKTableController.h
  13. +54 −0 Code/RKObjectManager+RKTableController.m
  14. +43 −0 Code/RKRefreshGestureRecognizer.h
  15. +241 −0 Code/RKRefreshGestureRecognizer.m
  16. +42 −0 Code/RKRefreshTriggerView.h
  17. +188 −0 Code/RKRefreshTriggerView.m
  18. +15 −0 Code/RKTableCellBlockTypes.h
  19. +153 −0 Code/RKTableController.h
  20. +531 −0 Code/RKTableController.m
  21. +156 −0 Code/RKTableItem.h
  22. +147 −0 Code/RKTableItem.m
  23. +59 −0 Code/RKTableSection.h
  24. +159 −0 Code/RKTableSection.m
  25. +33 −0 Code/RKTableUI.h
  26. +235 −0 Code/RKTableViewCellMapping.h
  27. +187 −0 Code/RKTableViewCellMapping.m
  28. +33 −0 Code/RKTableViewCellMappings.h
  29. +79 −0 Code/RKTableViewCellMappings.m
  30. +42 −0 Code/UIImage+RKAdditions.h
  31. +38 −0 Code/UIImage+RKAdditions.m
  32. +27 −0 Code/UIView+FindFirstResponder.h
  33. +41 −0 Code/UIView+FindFirstResponder.m
  34. +202 −0 LICENSE
  35. +19 −0 RKTableController.podspec
  36. BIN  Resources/RKRefreshTriggerViewAssets/blackArrow.png
  37. BIN  Resources/RKRefreshTriggerViewAssets/blackArrow@2x.png
  38. BIN  Resources/RKRefreshTriggerViewAssets/blueArrow.png
  39. BIN  Resources/RKRefreshTriggerViewAssets/blueArrow@2x.png
  40. BIN  Resources/RKRefreshTriggerViewAssets/grayArrow.png
  41. BIN  Resources/RKRefreshTriggerViewAssets/grayArrow@2x.png
  42. BIN  Resources/RKRefreshTriggerViewAssets/whiteArrow.png
  43. BIN  Resources/RKRefreshTriggerViewAssets/whiteArrow@2x.png
  44. +60 −0 Resources/RestKitResources.bundle/Contents/Info.plist
  45. BIN  Resources/RestKitResources.bundle/Contents/Resources/blackArrow.png
  46. BIN  Resources/RestKitResources.bundle/Contents/Resources/blackArrow@2x.png
  47. BIN  Resources/RestKitResources.bundle/Contents/Resources/blueArrow.png
  48. BIN  Resources/RestKitResources.bundle/Contents/Resources/blueArrow@2x.png
  49. BIN  Resources/RestKitResources.bundle/Contents/Resources/grayArrow.png
  50. BIN  Resources/RestKitResources.bundle/Contents/Resources/grayArrow@2x.png
  51. BIN  Resources/RestKitResources.bundle/Contents/Resources/whiteArrow.png
  52. BIN  Resources/RestKitResources.bundle/Contents/Resources/whiteArrow@2x.png
49 Code/NSBundle+RKTableControllerAdditions.h
View
@@ -0,0 +1,49 @@
+//
+// NSBundle+RKTableControllerAdditions.h
+// RestKit
+//
+// Created by Blake Watters on 8/28/12.
+// Copyright (c) 2009-2012 RestKit. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import <UIKit/UIImage.h>
+
+/**
+ Provides convenience methods for accessing data in resources
+ within an NSBundle.
+ */
+@interface NSBundle (RKTableControllerAdditions)
+
+/**
+ Returns an NSBundle reference to the RestKitResources.bundle file containing
+ RestKit specific resource assets.
+
+ This method is a convenience wrapper for invoking
+ `[NSBundle bundleWithIdentifier:@"org.restkit.RestKitResources"]`
+
+ @return An NSBundle object corresponding to the RestKitResources.bundle file.
+ */
++ (NSBundle *)restKitResourcesBundle;
+
+/**
+ Creates and returns an image object by loading the image data from the resource identified by the specified name and file extension.
+
+ @param name The name of the resource file.
+ @param extension If extension is an empty string or nil, the extension is assumed not to exist and the file is the first file encountered that exactly matches name.
+ @return A new image object for the specified file, or nil if the method could not initialize the image from the specified file.
+ */
+- (UIImage *)imageWithContentsOfResource:(NSString *)name withExtension:(NSString *)extension;
+
+@end
53 Code/NSBundle+RKTableControllerAdditions.m
View
@@ -0,0 +1,53 @@
+//
+// NSBundle+RKTableControllerAdditions.m
+// RestKit
+//
+// Created by Blake Watters on 8/28/12.
+// Copyright (c) 2009-2012 RestKit. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import "NSBundle+RKTableControllerAdditions.h"
+#import "UIImage+RKAdditions.h"
+#import "RKLog.h"
+
+@implementation NSBundle (RKTableControllerAdditions)
+
++ (NSBundle *)restKitResourcesBundle
+{
+ static BOOL searchedForBundle = NO;
+
+ if (! searchedForBundle) {
+ NSString *path = [[NSBundle mainBundle] pathForResource:@"RestKitResources" ofType:@"bundle"];
+ searchedForBundle = YES;
+ NSBundle *resourcesBundle = [NSBundle bundleWithPath:path];
+ if (! resourcesBundle) RKLogWarning(@"Unable to find RestKitResources.bundle in your project. Did you forget to add it?");
+ return resourcesBundle;
+ }
+
+ return [NSBundle bundleWithIdentifier:@"org.restkit.RestKitResources"];
+}
+
+- (UIImage *)imageWithContentsOfResource:(NSString *)name withExtension:(NSString *)extension
+{
+ NSString *resourcePath = [self pathForResource:name ofType:extension];
+ if (! resourcePath) {
+ RKLogWarning(@"%@ Failed to locate Resource with name '%@' and extension '%@': File Not Found.", self, resourcePath, extension);
+ return nil;
+ }
+
+ return [UIImage imageWithContentsOfResolutionIndependentFile:resourcePath];
+}
+
+@end
444 Code/RKAbstractTableController.h
View
@@ -0,0 +1,444 @@
+//
+// RKAbstractTableController.h
+// RestKit
+//
+// Created by Jeff Arena on 8/11/11.
+// Copyright (c) 2009-2012 RestKit. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#if TARGET_OS_IPHONE
+
+#import <UIKit/UIKit.h>
+#import "RKTableCellBlockTypes.h"
+#import "RKTableViewCellMappings.h"
+#import "RKTableItem.h"
+#import "RKObjectMapping.h"
+#import "RKKeyboardScroller.h"
+
+///-----------------------------------------------------------------------------
+/// @name Constants
+///-----------------------------------------------------------------------------
+
+/**
+ Posted when the table controller starts loading.
+ */
+extern NSString * const RKTableControllerDidStartLoadNotification;
+
+/**
+ Posted when the table controller finishes loading.
+ */
+extern NSString * const RKTableControllerDidFinishLoadNotification;
+
+/**
+ Posted when the table controller has loaded objects into the table view.
+ */
+extern NSString * const RKTableControllerDidLoadObjectsNotification;
+
+/**
+ Posted when the table controller has loaded an empty collection of objects into the table view.
+ */
+extern NSString * const RKTableControllerDidLoadEmptyNotification;
+
+/**
+ Posted when the table controller has loaded an error.
+ */
+extern NSString * const RKTableControllerDidLoadErrorNotification;
+
+/**
+ Posted when the table controller has transitioned from an offline to online state.
+ */
+extern NSString * const RKTableControllerDidBecomeOnline;
+
+/**
+ Posted when the table controller has transitioned from an online to an offline state.
+ */
+extern NSString * const RKTableControllerDidBecomeOffline;
+
+@protocol RKAbstractTableControllerDelegate;
+
+/**
+ @enum RKTableControllerState
+
+ @constant RKTableControllerStateNormal Indicates that the table has
+ loaded normally and is displaying cell content. It is not loading content,
+ is not empty, has not loaded an error, and is not offline.
+
+ @constant RKTableControllerStateLoading Indicates that the table controller
+ is loading content from a remote source.
+
+ @constant RKTableControllerStateEmpty Indicates that the table controller has
+ retrieved an empty collection of objects.
+
+ @constant RKTableControllerStateError Indicates that the table controller has
+ encountered an error while attempting to load.
+
+ @constant RKTableControllerStateOffline Indicates that the table controller is
+ offline and cannot perform network access.
+
+ @constant RKTableControllerStateNotYetLoaded Indicates that the table controller is
+ has not yet attempted a load and state is unknown.
+ */
+enum {
+ RKTableControllerStateNormal = 0,
+ RKTableControllerStateLoading = 1 << 1,
+ RKTableControllerStateEmpty = 1 << 2,
+ RKTableControllerStateError = 1 << 3,
+ RKTableControllerStateOffline = 1 << 4,
+ RKTableControllerStateNotYetLoaded = 0xFF000000
+};
+typedef NSUInteger RKTableControllerState;
+
+/**
+ RKAbstractTableController is an abstract base class for concrete table controller classes.
+ A table controller object acts as both the delegate and data source for a UITableView
+ object and leverages the RestKit object mapping engine to transform local domain models
+ into UITableViewCell representations. Concrete implementations are provided for the
+ display of static table views and Core Data backed fetched results controller basied
+ table views.
+ */
+@interface RKAbstractTableController : NSObject <UITableViewDataSource, UITableViewDelegate>
+
+///-----------------------------------------------------------------------------
+/// @name Configuring the Table Controller
+///-----------------------------------------------------------------------------
+
+@property (nonatomic, weak) id<RKAbstractTableControllerDelegate> delegate;
+@property (weak, nonatomic, readonly) UIViewController *viewController;
+@property (weak, nonatomic, readonly) UITableView *tableView;
+@property (nonatomic, assign) UITableViewRowAnimation defaultRowAnimation;
+
+@property (nonatomic, assign) BOOL pullToRefreshEnabled;
+@property (nonatomic, assign) BOOL canEditRows;
+@property (nonatomic, assign) BOOL canMoveRows;
+@property (nonatomic, assign) BOOL autoResizesForKeyboard;
+
+///-----------------------------------------------------------------------------
+/// @name Instantiation
+///-----------------------------------------------------------------------------
+
++ (id)tableControllerWithTableView:(UITableView *)tableView
+ forViewController:(UIViewController *)viewController;
+
++ (id)tableControllerForTableViewController:(UITableViewController *)tableViewController;
+
+- (id)initWithTableView:(UITableView *)tableView
+ viewController:(UIViewController *)viewController;
+
+///-----------------------------------------------------------------------------
+/// @name Object to Table View Cell Mappings
+///-----------------------------------------------------------------------------
+
+@property (nonatomic, strong) RKTableViewCellMappings *cellMappings;
+
+- (void)mapObjectsWithClass:(Class)objectClass toTableCellsWithMapping:(RKTableViewCellMapping *)cellMapping;
+- (void)mapObjectsWithClassName:(NSString *)objectClassName toTableCellsWithMapping:(RKTableViewCellMapping *)cellMapping;
+- (id)objectForRowAtIndexPath:(NSIndexPath *)indexPath;
+- (RKTableViewCellMapping *)cellMappingForObjectAtIndexPath:(NSIndexPath *)indexPath;
+
+/**
+ Return the index path of the object within the table
+ */
+- (NSIndexPath *)indexPathForObject:(id)object;
+- (UITableViewCell *)cellForObject:(id)object;
+- (void)reloadRowForObject:(id)object withRowAnimation:(UITableViewRowAnimation)rowAnimation;
+
+///-----------------------------------------------------------------------------
+/// @name Header and Footer Rows
+///-----------------------------------------------------------------------------
+
+- (void)addHeaderRowForItem:(RKTableItem *)tableItem;
+- (void)addFooterRowForItem:(RKTableItem *)tableItem;
+- (void)addHeaderRowWithMapping:(RKTableViewCellMapping *)cellMapping;
+- (void)addFooterRowWithMapping:(RKTableViewCellMapping *)cellMapping;
+- (void)removeAllHeaderRows;
+- (void)removeAllFooterRows;
+
+///-----------------------------------------------------------------------------
+/// @name RESTful Table Loading
+///-----------------------------------------------------------------------------
+
+@property (nonatomic, strong) NSOperationQueue *operationQueue;
+@property (nonatomic, strong) NSArray *responseDescriptors;
+@property (nonatomic, assign) BOOL autoRefreshFromNetwork;
+@property (nonatomic, assign) NSTimeInterval autoRefreshRate;
+
+- (void)cancelLoad;
+- (BOOL)isAutoRefreshNeeded;
+
+///-----------------------------------------------------------------------------
+/// @name Inspecting Table State
+///-----------------------------------------------------------------------------
+
+/**
+ The current state of the table controller. Note that the controller may be in more
+ than one state (e.g. loading | empty).
+ */
+@property (nonatomic, readonly, assign) RKTableControllerState state;
+
+/**
+ An error object that was encountered as the result of an attempt to load
+ the table. Will return a value when the table is in the error state,
+ otherwise nil.
+ */
+@property (nonatomic, readonly, strong) NSError *error;
+
+/**
+ Returns a Boolean value indicating if the table controller is currently
+ loading content.
+ */
+- (BOOL)isLoading;
+
+/**
+ Returns a Boolean value indicating if the table controller has attempted
+ a load and transitioned into any state.
+ */
+- (BOOL)isLoaded;
+
+/**
+ Returns a Boolean value indicating if the table controller has loaded an
+ empty set of content.
+
+ When YES and there is not an empty item configured, the table controller
+ will optionally display an empty image overlayed on top of the table view.
+
+ **NOTE**: It is possible for an empty table controller to display cells
+ witin the managed table view in the event an empty item or header/footer
+ rows are configured.
+
+ @see imageForEmpty
+ */
+- (BOOL)isEmpty;
+
+/**
+ Returns a Boolean value indicating if the table controller is online
+ and network operations may be performed.
+ */
+- (BOOL)isOnline;
+
+/**
+ Returns a Boolean value indicating if the table controller is offline.
+
+ When YES, the table controller will optionally display an offline image
+ overlayed on top of the table view.
+
+ @see imageForOffline
+ */
+- (BOOL)isOffline;
+
+/**
+ Returns a Boolean value indicating if the table controller encountered
+ an error while attempting to load.
+
+ When YES, the table controller will optionally display an error image
+ overlayed on top of the table view.
+
+ @see imageForError
+ */
+- (BOOL)isError;
+
+///-----------------------------------------------------------------------------
+/// @name Block Callbacks
+///-----------------------------------------------------------------------------
+
+// TODO: Audit and expand the library of callbacks...
+// TODO: Docs AND tests...
+@property (nonatomic, copy) RKTableCellForObjectAtIndexPathBlock onSelectCellForObjectAtIndexPath;
+@property (nonatomic, copy) RKTableCellForObjectAtIndexPathBlock onPrepareCellForObjectAtIndexPath; // TODO: May want to eliminate...
+@property (nonatomic, copy) RKTableCellForObjectAtIndexPathBlock onWillDisplayCellForObjectAtIndexPath;
+
+- (void)setOnSelectCellForObjectAtIndexPath:(RKTableCellForObjectAtIndexPathBlock)onSelectCellForObjectAtIndexPath;
+- (void)setOnPrepareCellForObjectAtIndexPath:(RKTableCellForObjectAtIndexPathBlock)onPrepareCellForObjectAtIndexPath;
+- (void)setOnWillDisplayCellForObjectAtIndexPath:(RKTableCellForObjectAtIndexPathBlock)onWillDisplayCellForObjectAtIndexPath;
+
+///-----------------------------------------------------------------------------
+/// @name Model State Views
+///-----------------------------------------------------------------------------
+
+/**
+ An image to overlay onto the table when the table view
+ does not have any row data to display. It will be centered
+ within the table view.
+ */
+@property (nonatomic, strong) UIImage *imageForEmpty;
+
+/**
+ An image to overlay onto the table when a load operation
+ has encountered an error. It will be centered
+ within the table view.
+ */
+@property (nonatomic, strong) UIImage *imageForError;
+
+/**
+ An image to overlay onto the table with when the user does
+ not have connectivity to the Internet.
+
+ @see RKReachabilityObserver
+ */
+@property (nonatomic, strong) UIImage *imageForOffline;
+
+/**
+ A UIView to add to the table overlay during loading. It
+ will be positioned directly in the center of the table view.
+
+ The loading view is always presented non-modally.
+ */
+@property (nonatomic, strong) UIView *loadingView;
+
+/**
+ Returns the image, if any, configured for display when the table controller
+ is in the given state.
+
+ **NOTE** This method accepts a single state value.
+
+ @param state The table controller state
+ @return The image for the specified state, else nil. Always returns nil for
+ RKTableControllerStateNormal, RKTableControllerStateLoading and RKTableControllerStateLoading.
+ */
+- (UIImage *)imageForState:(RKTableControllerState)state;
+
+/**
+ A rectangle configuring the dimensions for the overlay view that is
+ applied to the table view during display of the loading view and
+ state overlay images (offline/error/empty). By default, the overlay
+ view will be auto-sized to cover the entire table. This can result in
+ an inaccessible table UI if you have embedded controls within the header
+ or footer views of your table. You can adjust the frame of the overlay
+ precisely by configuring the overlayFrame property.
+ */
+@property (nonatomic, assign) CGRect overlayFrame;
+
+/**
+ The image currently displayed within the overlay view.
+ */
+@property (weak, nonatomic, readonly) UIImage *overlayImage;
+
+/**
+ When YES, the image view added to the table overlay for displaying table
+ state (i.e. for offline, error and empty) will be displayed modally
+ and prevent any interaction with the table.
+
+ **Default**: YES
+ */
+@property (nonatomic, assign) BOOL showsOverlayImagesModally;
+
+// Default NO
+@property (nonatomic, assign) BOOL variableHeightRows;
+@property (nonatomic, assign) BOOL showsHeaderRowsWhenEmpty;
+@property (nonatomic, assign) BOOL showsFooterRowsWhenEmpty;
+@property (nonatomic, strong) RKTableItem *emptyItem;
+
+///-----------------------------------------------------------------------------
+/// @name Managing Sections
+///-----------------------------------------------------------------------------
+
+/**
+ The number of sections in the table.
+ */
+@property (nonatomic, readonly) NSUInteger sectionCount;
+
+/**
+ The number of rows across all sections in the model.
+ */
+@property (nonatomic, readonly) NSUInteger rowCount;
+
+/**
+ Returns the number of rows in the section at the given index.
+
+ @param index The index of the section to return the row count for.
+ @returns The number of rows contained within the section with the given index.
+ @raises NSInvalidArgumentException Raised if index is greater than or
+ equal to the total number of sections in the table.
+ */
+- (NSUInteger)numberOfRowsInSection:(NSUInteger)index;
+
+///-----------------------------------------------------------------------------
+/// @name Managing Swipe View
+///-----------------------------------------------------------------------------
+
+@property (nonatomic, assign) BOOL cellSwipeViewsEnabled;
+@property (nonatomic, strong) UIView *cellSwipeView;
+@property (nonatomic, readonly) UITableViewCell *swipeCell;
+@property (nonatomic, readonly) id swipeObject;
+@property (nonatomic, readonly) BOOL animatingCellSwipe;
+@property (nonatomic, readonly) UISwipeGestureRecognizerDirection swipeDirection;
+
+- (void)addSwipeViewTo:(UITableViewCell *)cell withObject:(id)object direction:(UISwipeGestureRecognizerDirection)direction;
+- (void)removeSwipeView:(BOOL)animated;
+
+@end
+
+@protocol RKAbstractTableControllerDelegate <NSObject>
+
+@optional
+
+// Network
+//- (void)tableController:(RKAbstractTableController *)tableController willLoadTableWithObjectLoader:(RKObjectLoader *)objectLoader;
+//- (void)tableController:(RKAbstractTableController *)tableController didLoadTableWithObjectLoader:(RKObjectLoader *)objectLoader;
+
+// Basic States
+- (void)tableControllerDidStartLoad:(RKAbstractTableController *)tableController;
+
+/**
+ Sent when the table view has transitioned out of the loading state regardless of outcome
+ */
+- (void)tableControllerDidFinishLoad:(RKAbstractTableController *)tableController;
+- (void)tableController:(RKAbstractTableController *)tableController didFailLoadWithError:(NSError *)error;
+- (void)tableControllerDidCancelLoad:(RKAbstractTableController *)tableController;
+
+/**
+ Sent to the delegate when the controller is really and truly finished loading/updating, whether from the network or from Core Data,
+ or from static data, ... this happens in didFinishLoading
+ */
+- (void)tableControllerDidFinalizeLoad:(RKAbstractTableController *)tableController;
+
+/**
+ Sent to the delegate when the content of the table view has become empty
+ */
+- (void)tableControllerDidBecomeEmpty:(RKAbstractTableController *)tableController;
+
+/**
+ Sent to the delegate when the table controller has transitioned from offline to online
+ */
+- (void)tableControllerDidBecomeOnline:(RKAbstractTableController *)tableController;
+
+/**
+ Sent to the delegate when the table controller has transitioned from online to offline
+ */
+- (void)tableControllerDidBecomeOffline:(RKAbstractTableController *)tableController;
+
+// Objects
+- (void)tableController:(RKAbstractTableController *)tableController didInsertObject:(id)object atIndexPath:(NSIndexPath *)indexPath;
+- (void)tableController:(RKAbstractTableController *)tableController didUpdateObject:(id)object atIndexPath:(NSIndexPath *)indexPath;
+- (void)tableController:(RKAbstractTableController *)tableController didDeleteObject:(id)object atIndexPath:(NSIndexPath *)indexPath;
+
+// Editing
+- (void)tableController:(RKAbstractTableController *)tableController willBeginEditing:(id)object atIndexPath:(NSIndexPath *)indexPath;
+- (void)tableController:(RKAbstractTableController *)tableController didEndEditing:(id)object atIndexPath:(NSIndexPath *)indexPath;
+
+// Swipe Views
+- (void)tableController:(RKAbstractTableController *)tableController willAddSwipeView:(UIView *)swipeView toCell:(UITableViewCell *)cell forObject:(id)object;
+- (void)tableController:(RKAbstractTableController *)tableController willRemoveSwipeView:(UIView *)swipeView fromCell:(UITableViewCell *)cell forObject:(id)object;
+
+// Cells
+- (void)tableController:(RKAbstractTableController *)tableController willDisplayCell:(UITableViewCell *)cell forObject:(id)object atIndexPath:(NSIndexPath *)indexPath;
+- (void)tableController:(RKAbstractTableController *)tableController didSelectCell:(UITableViewCell *)cell forObject:(id)object atIndexPath:(NSIndexPath *)indexPath;
+
+// Sections
+- (CGFloat)tableController:(RKAbstractTableController *)tableController heightForHeaderInSection:(NSInteger)sectionIndex;
+- (CGFloat)tableController:(RKAbstractTableController *)tableController heightForFooterInSection:(NSInteger)sectionIndex;
+
+@end
+
+#endif // TARGET_OS_IPHONE
1,338 Code/RKAbstractTableController.m
View
@@ -0,0 +1,1338 @@
+//
+// RKAbstractTableController.m
+// RestKit
+//
+// Created by Jeff Arena on 8/11/11.
+// Copyright (c) 2009-2012 RestKit. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import "RKAbstractTableController.h"
+#import "RKAbstractTableController_Internals.h"
+#import "RKMappingOperation.h"
+#import "RKLog.h"
+#import "RKErrors.h"
+#import "RKReachabilityObserver.h"
+#import "UIView+FindFirstResponder.h"
+#import "RKRefreshGestureRecognizer.h"
+#import "RKTableSection.h"
+#import "RKObjectRequestOperation.h"
+#import "RKObjectMappingOperationDataSource.h"
+
+// Define logging component
+#undef RKLogComponent
+#define RKLogComponent lcl_cRestKitUI
+
+/**
+ Bounce pixels define how many pixels the cell swipe view is
+ moved during the bounce animation
+ */
+#define BOUNCE_PIXELS 5.0
+
+NSString * const RKTableControllerDidStartLoadNotification = @"RKTableControllerDidStartLoadNotification";
+NSString * const RKTableControllerDidFinishLoadNotification = @"RKTableControllerDidFinishLoadNotification";
+NSString * const RKTableControllerDidLoadObjectsNotification = @"RKTableControllerDidLoadObjectsNotification";
+NSString * const RKTableControllerDidLoadEmptyNotification = @"RKTableControllerDidLoadEmptyNotification";
+NSString * const RKTableControllerDidLoadErrorNotification = @"RKTableControllerDidLoadErrorNotification";
+NSString * const RKTableControllerDidBecomeOnline = @"RKTableControllerDidBecomeOnline";
+NSString * const RKTableControllerDidBecomeOffline = @"RKTableControllerDidBecomeOffline";
+
+static NSString *lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey";
+
+@interface RKAbstractTableController ()
+
+@property (nonatomic, strong) RKKeyboardScroller *keyboardScroller;
+
+@end
+
+
+@implementation RKAbstractTableController
+
+#pragma mark - Instantiation
+
++ (id)tableControllerWithTableView:(UITableView *)tableView
+ forViewController:(UIViewController *)viewController
+{
+ return [[self alloc] initWithTableView:tableView viewController:viewController];
+}
+
++ (id)tableControllerForTableViewController:(UITableViewController *)tableViewController
+{
+ return [self tableControllerWithTableView:tableViewController.tableView
+ forViewController:tableViewController];
+}
+
+- (id)initWithTableView:(UITableView *)theTableView viewController:(UIViewController *)theViewController
+{
+ NSAssert(theTableView, @"Cannot initialize a table view model with a nil tableView");
+ NSAssert(theViewController, @"Cannot initialize a table view model with a nil viewController");
+ self = [self init];
+ if (self) {
+ self.tableView = theTableView;
+ _viewController = theViewController; // Assign directly to avoid side-effect of overloaded accessor method
+ self.variableHeightRows = NO;
+ self.defaultRowAnimation = UITableViewRowAnimationFade;
+ self.overlayFrame = CGRectZero;
+ self.showsOverlayImagesModally = YES;
+ }
+
+ return self;
+}
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ if ([self isMemberOfClass:[RKAbstractTableController class]]) {
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
+ reason:[NSString stringWithFormat:@"%@ is abstract. Instantiate one its subclasses instead.",
+ NSStringFromClass([self class])]
+ userInfo:nil];
+ }
+
+ self.state = RKTableControllerStateNotYetLoaded;
+ _cellMappings = [RKTableViewCellMappings new];
+
+ _headerItems = [NSMutableArray new];
+ _footerItems = [NSMutableArray new];
+ _showsHeaderRowsWhenEmpty = YES;
+ _showsFooterRowsWhenEmpty = YES;
+
+ // Setup autoRefreshRate to (effectively) never
+ _autoRefreshFromNetwork = NO;
+ _autoRefreshRate = NSTimeIntervalSince1970;
+
+ // Setup key-value observing
+ [self addObserver:self
+ forKeyPath:@"state"
+ options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
+ context:nil];
+ [self addObserver:self
+ forKeyPath:@"error"
+ options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
+ context:nil];
+ }
+ return self;
+}
+
+- (void)dealloc
+{
+ // Disconnect from the tableView
+ if (_tableView.delegate == self) _tableView.delegate = nil;
+ if (_tableView.dataSource == self) _tableView.dataSource = nil;
+
+ // Remove overlay and pull-to-refresh subviews
+ [_stateOverlayImageView removeFromSuperview];
+ [_tableOverlayView removeFromSuperview];
+
+ // Remove observers
+ [self removeObserver:self forKeyPath:@"state"];
+ [self removeObserver:self forKeyPath:@"error"];
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+ [self.objectRequestOperation cancel];
+}
+
+- (void)setTableView:(UITableView *)tableView
+{
+ NSAssert(tableView, @"Cannot assign a nil tableView to the model");
+ _tableView = tableView;
+ _tableView.delegate = self;
+ _tableView.dataSource = self;
+}
+
+- (void)setViewController:(UIViewController *)viewController
+{
+ if ([viewController isKindOfClass:[UITableViewController class]]) {
+ self.tableView = [(UITableViewController *)viewController tableView];
+ }
+}
+
+//- (void)setObjectManager:(RKObjectManager *)objectManager
+//{
+// NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
+//
+// // Remove observers
+// if (_objectManager) {
+//// [notificationCenter removeObserver:self
+//// name:RKObjectManagerDidBecomeOfflineNotification
+//// object:_objectManager];
+//// [notificationCenter removeObserver:self
+//// name:RKObjectManagerDidBecomeOnlineNotification
+//// object:_objectManager];
+// }
+//
+// _objectManager = objectManager;
+//
+// if (objectManager) {
+// // Set observers
+//// [notificationCenter addObserver:self
+//// selector:@selector(objectManagerConnectivityDidChange:)
+//// name:RKObjectManagerDidBecomeOnlineNotification
+//// object:objectManager];
+//// [notificationCenter addObserver:self
+//// selector:@selector(objectManagerConnectivityDidChange:)
+//// name:RKObjectManagerDidBecomeOfflineNotification
+//// object:objectManager];
+//
+// // Initialize online/offline state (if it is known)
+//// if (objectManager.networkStatus != RKObjectManagerNetworkStatusUnknown) {
+//// if (objectManager.isOnline) {
+//// self.state &= ~RKTableControllerStateOffline;
+//// } else {
+//// self.state |= RKTableControllerStateOffline;
+//// }
+//// }
+// }
+//}
+
+- (void)setAutoResizesForKeyboard:(BOOL)autoResizesForKeyboard
+{
+ if (_autoResizesForKeyboard != autoResizesForKeyboard) {
+ _autoResizesForKeyboard = autoResizesForKeyboard;
+ if (_autoResizesForKeyboard) {
+ self.keyboardScroller = [[RKKeyboardScroller alloc] initWithViewController:self.viewController scrollView:self.tableView];
+ self.keyboardScroller;
+ } else {
+ self.keyboardScroller = nil;
+ }
+ }
+}
+
+- (void)setAutoRefreshFromNetwork:(BOOL)autoRefreshFromNetwork
+{
+ if (_autoRefreshFromNetwork != autoRefreshFromNetwork) {
+ _autoRefreshFromNetwork = autoRefreshFromNetwork;
+// if (_autoRefreshFromNetwork) {
+// NSString *cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
+// stringByAppendingPathComponent:@"RKAbstractTableControllerCache"];
+// _cache = [[RKCache alloc] initWithPath:cachePath subDirectories:nil];
+// } else {
+// if (_cache) {
+// [_cache invalidateAll];
+// [_cache release];
+// _cache = nil;
+// }
+// }
+ }
+}
+
+- (void)setLoading:(BOOL)loading
+{
+ if (loading) {
+ self.state |= RKTableControllerStateLoading;
+ } else {
+ self.state &= ~RKTableControllerStateLoading;
+ }
+}
+
+// NOTE: The loaded flag is handled specially. When loaded becomes NO,
+// we clear all other flags. In practice this should not happen outside of init.
+- (void)setLoaded:(BOOL)loaded
+{
+ if (loaded) {
+ self.state &= ~RKTableControllerStateNotYetLoaded;
+ } else {
+ self.state = RKTableControllerStateNotYetLoaded;
+ }
+}
+
+- (void)setEmpty:(BOOL)empty
+{
+ if (empty) {
+ self.state |= RKTableControllerStateEmpty;
+ } else {
+ self.state &= ~RKTableControllerStateEmpty;
+ }
+}
+
+- (void)setOffline:(BOOL)offline
+{
+ if (offline) {
+ self.state |= RKTableControllerStateOffline;
+ } else {
+ self.state &= ~RKTableControllerStateOffline;
+ }
+}
+
+- (void)setErrorState:(BOOL)error
+{
+ if (error) {
+ self.state |= RKTableControllerStateError;
+ } else {
+ self.state &= ~RKTableControllerStateError;
+ }
+}
+
+- (void)objectManagerConnectivityDidChange:(NSNotification *)notification
+{
+// RKLogTrace(@"%@ received network status change notification: %@", self, [notification name]);
+// [self setOffline:!self.objectManager.isOnline];
+}
+
+#pragma mark - Abstract Methods
+
+- (BOOL)isConsideredEmpty
+{
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
+ reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)]
+ userInfo:nil];
+}
+
+- (NSUInteger)sectionCount
+{
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
+ reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)]
+ userInfo:nil];
+}
+
+- (NSUInteger)rowCount
+{
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
+ reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)]
+ userInfo:nil];
+}
+
+- (id)objectForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
+ reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)]
+ userInfo:nil];
+}
+
+- (NSIndexPath *)indexPathForObject:(id)object
+{
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
+ reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)]
+ userInfo:nil];
+}
+
+- (NSUInteger)numberOfRowsInSection:(NSUInteger)index
+{
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
+ reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)]
+ userInfo:nil];
+}
+
+#pragma mark - Cell Mappings
+
+- (void)mapObjectsWithClass:(Class)objectClass toTableCellsWithMapping:(RKTableViewCellMapping *)cellMapping
+{
+ // TODO: Should we raise an exception/throw a warning if you are doing class mapping for a type
+ // that implements a cellMapping instance method? Maybe a class declaration overrides
+ [_cellMappings setCellMapping:cellMapping forClass:objectClass];
+}
+
+- (void)mapObjectsWithClassName:(NSString *)objectClassName toTableCellsWithMapping:(RKTableViewCellMapping *)cellMapping
+{
+ [self mapObjectsWithClass:NSClassFromString(objectClassName) toTableCellsWithMapping:cellMapping];
+}
+
+- (RKTableViewCellMapping *)cellMappingForObjectAtIndexPath:(NSIndexPath *)indexPath
+{
+ NSAssert(indexPath, @"Cannot lookup cell mapping for object with a nil indexPath");
+ id object = [self objectForRowAtIndexPath:indexPath];
+ return [self.cellMappings cellMappingForObject:object];
+}
+
+- (UITableViewCell *)cellForObject:(id)object
+{
+ NSIndexPath *indexPath = [self indexPathForObject:object];
+ return indexPath ? [self.tableView cellForRowAtIndexPath:indexPath] : nil;
+}
+
+#pragma mark - Header and Footer Rows
+
+- (void)addHeaderRowForItem:(RKTableItem *)tableItem
+{
+ [_headerItems addObject:tableItem];
+}
+
+- (void)addFooterRowForItem:(RKTableItem *)tableItem
+{
+ [_footerItems addObject:tableItem];
+}
+
+- (void)addHeaderRowWithMapping:(RKTableViewCellMapping *)cellMapping
+{
+ RKTableItem *tableItem = [RKTableItem tableItem];
+ tableItem.cellMapping = cellMapping;
+ [self addHeaderRowForItem:tableItem];
+}
+
+- (void)addFooterRowWithMapping:(RKTableViewCellMapping *)cellMapping
+{
+ RKTableItem *tableItem = [RKTableItem tableItem];
+ tableItem.cellMapping = cellMapping;
+ [self addFooterRowForItem:tableItem];
+}
+
+- (void)removeAllHeaderRows
+{
+ [_headerItems removeAllObjects];
+}
+
+- (void)removeAllFooterRows
+{
+ [_footerItems removeAllObjects];
+}
+
+#pragma mark - UITableViewDataSource methods
+
+- (UITableViewCell *)cellFromCellMapping:(RKTableViewCellMapping *)cellMapping
+{
+ RKLogTrace(@"About to dequeue reusable cell using self.reuseIdentifier=%@", cellMapping.reuseIdentifier);
+ UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:cellMapping.reuseIdentifier];
+ if (cell) {
+ RKLogTrace(@"Dequeued existing cell object for reuse identifier '%@': %@", cellMapping.reuseIdentifier, cell);
+ } else {
+ cell = [[cellMapping.objectClass alloc] initWithStyle:cellMapping.style
+ reuseIdentifier:cellMapping.reuseIdentifier];
+ RKLogTrace(@"Failed to dequeue existing cell object for reuse identifier '%@', instantiated new cell: %@", cellMapping.reuseIdentifier, cell);
+ }
+
+ if (cellMapping.managesCellAttributes) {
+ cell.accessoryType = cellMapping.accessoryType;
+ cell.selectionStyle = cellMapping.selectionStyle;
+ }
+
+ // Fire the prepare callbacks
+ for (void (^block)(UITableViewCell *) in cellMapping.prepareCellBlocks) {
+ block(cell);
+ }
+
+ return cell;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)theTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ NSAssert(theTableView == self.tableView, @"tableView:cellForRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView);
+ NSAssert(indexPath, @"Cannot retrieve cell for nil indexPath");
+ id mappableObject = [self objectForRowAtIndexPath:indexPath];
+ NSAssert(mappableObject, @"Cannot build a tableView cell without an object");
+
+ RKTableViewCellMapping* cellMapping = [self.cellMappings cellMappingForObject:mappableObject];
+ NSAssert(cellMapping, @"Cannot build a tableView cell for object %@: No cell mapping defined for objects of type '%@'", mappableObject, NSStringFromClass([mappableObject class]));
+
+ UITableViewCell *cell = [self cellFromCellMapping:cellMapping];
+ NSAssert(cell, @"Cell mapping failed to dequeue or allocate a tableViewCell for object: %@", mappableObject);
+
+ // Map the object state into the cell
+ RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new];
+ RKMappingOperation* mappingOperation = [[RKMappingOperation alloc] initWithSourceObject:mappableObject destinationObject:cell mapping:cellMapping];
+ mappingOperation.dataSource = dataSource;
+ NSError* error = nil;
+ BOOL success = [mappingOperation performMapping:&error];
+
+ // NOTE: If there is no mapping work performed, but no error is generated then
+ // we consider the operation a success. It is common for table cells to not contain
+ // any dynamically mappable content (i.e. header/footer rows, banners, etc.)
+ if (success == NO && error != nil) {
+ RKLogError(@"Failed table cell mapping: %@", error);
+ }
+
+ if (self.onPrepareCellForObjectAtIndexPath) {
+ self.onPrepareCellForObjectAtIndexPath(cell, mappableObject, indexPath);
+ }
+
+ RKLogTrace(@"%@ cellForRowAtIndexPath:%@ = %@", self, indexPath, cell);
+ return cell;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+ [NSException raise:@"Must be implemented in a subclass!" format:@"sectionCount must be implemented with a subclass"];
+ return 0;
+}
+
+#pragma mark - UITableViewDelegate methods
+
+- (void)tableView:(UITableView *)theTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ NSAssert(theTableView == self.tableView, @"tableView:didSelectRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView);
+ RKLogTrace(@"%@: Row at indexPath %@ selected for tableView %@", self, indexPath, theTableView);
+
+ id object = [self objectForRowAtIndexPath:indexPath];
+
+ UITableViewCell *cell = [theTableView cellForRowAtIndexPath:indexPath];
+ RKTableViewCellMapping *cellMapping = [_cellMappings cellMappingForObject:object];
+
+ // NOTE: Handle deselection first as the onSelectCell processing may result in the tableView
+ // being reloaded and our instances invalidated
+ if (cellMapping.deselectsRowOnSelection) {
+ [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
+ }
+
+ if (cellMapping.onSelectCell) {
+ cellMapping.onSelectCell();
+ }
+
+ if (cellMapping.onSelectCellForObjectAtIndexPath) {
+ RKLogTrace(@"%@: Invoking onSelectCellForObjectAtIndexPath block with cellMapping %@ for object %@ at indexPath = %@", self, cell, object, indexPath);
+ cellMapping.onSelectCellForObjectAtIndexPath(cell, object, indexPath);
+ }
+
+ // Table level selection callbacks
+ if (self.onSelectCellForObjectAtIndexPath) {
+ self.onSelectCellForObjectAtIndexPath(cell, object, indexPath);
+ }
+
+ if ([self.delegate respondsToSelector:@selector(tableController:didSelectCell:forObject:atIndexPath:)]) {
+ [self.delegate tableController:self didSelectCell:cell forObject:object atIndexPath:indexPath];
+ }
+}
+
+- (void)tableView:(UITableView *)theTableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ NSAssert(theTableView == self.tableView, @"tableView:didSelectRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView);
+ cell.hidden = NO;
+ id mappableObject = [self objectForRowAtIndexPath:indexPath];
+ RKTableViewCellMapping *cellMapping = [self.cellMappings cellMappingForObject:mappableObject];
+ if (cellMapping.onCellWillAppearForObjectAtIndexPath) {
+ cellMapping.onCellWillAppearForObjectAtIndexPath(cell, mappableObject, indexPath);
+ }
+
+ if (self.onWillDisplayCellForObjectAtIndexPath) {
+ self.onWillDisplayCellForObjectAtIndexPath(cell, mappableObject, indexPath);
+ }
+
+ if ([self.delegate respondsToSelector:@selector(tableController:willDisplayCell:forObject:atIndexPath:)]) {
+ [self.delegate tableController:self willDisplayCell:cell forObject:mappableObject atIndexPath:indexPath];
+ }
+
+ // Informal protocol
+ // TODO: Needs documentation!!!
+ SEL willDisplaySelector = @selector(willDisplayInTableViewCell:);
+ if ([mappableObject respondsToSelector:willDisplaySelector]) {
+ [mappableObject performSelector:willDisplaySelector withObject:cell];
+ }
+
+ // Handle hiding header/footer rows when empty
+ if ([self isEmpty]) {
+ if (! self.showsHeaderRowsWhenEmpty && [_headerItems containsObject:mappableObject]) {
+ cell.hidden = YES;
+ }
+
+ if (! self.showsFooterRowsWhenEmpty && [_footerItems containsObject:mappableObject]) {
+ cell.hidden = YES;
+ }
+ } else {
+ if (self.emptyItem && [self.emptyItem isEqual:mappableObject]) {
+ cell.hidden = YES;
+ }
+ }
+}
+
+// Variable height support
+
+- (CGFloat)tableView:(UITableView *)theTableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ if (self.variableHeightRows) {
+ RKTableViewCellMapping *cellMapping = [self cellMappingForObjectAtIndexPath:indexPath];
+
+ if (cellMapping.heightOfCellForObjectAtIndexPath) {
+ id object = [self objectForRowAtIndexPath:indexPath];
+ CGFloat height = cellMapping.heightOfCellForObjectAtIndexPath(object, indexPath);
+ RKLogTrace(@"Variable row height configured for tableView. Height via block invocation for row at indexPath '%@' = %f", indexPath, cellMapping.rowHeight);
+ return height;
+ } else {
+ RKLogTrace(@"Variable row height configured for tableView. Height for row at indexPath '%@' = %f", indexPath, cellMapping.rowHeight);
+ return cellMapping.rowHeight;
+ }
+ }
+
+ RKLogTrace(@"Uniform row height configured for tableView. Table view row height = %f", self.tableView.rowHeight);
+ return self.tableView.rowHeight;
+}
+
+- (void)tableView:(UITableView *)theTableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
+{
+ RKTableViewCellMapping *cellMapping = [self cellMappingForObjectAtIndexPath:indexPath];
+ if (cellMapping.onTapAccessoryButtonForObjectAtIndexPath) {
+ RKLogTrace(@"Found a block for tableView:accessoryButtonTappedForRowWithIndexPath: Executing...");
+ UITableViewCell *cell = [self tableView:self.tableView cellForRowAtIndexPath:indexPath];
+ id object = [self objectForRowAtIndexPath:indexPath];
+ cellMapping.onTapAccessoryButtonForObjectAtIndexPath(cell, object, indexPath);
+ }
+}
+
+- (NSString *)tableView:(UITableView *)theTableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ RKTableViewCellMapping *cellMapping = [self cellMappingForObjectAtIndexPath:indexPath];
+ if (cellMapping.titleForDeleteButtonForObjectAtIndexPath) {
+ RKLogTrace(@"Found a block for tableView:titleForDeleteConfirmationButtonForRowAtIndexPath: Executing...");
+ UITableViewCell *cell = [self tableView:self.tableView cellForRowAtIndexPath:indexPath];
+ id object = [self objectForRowAtIndexPath:indexPath];
+ return cellMapping.titleForDeleteButtonForObjectAtIndexPath(cell, object, indexPath);
+ }
+ return NSLocalizedString(@"Delete", nil);
+}
+
+- (UITableViewCellEditingStyle)tableView:(UITableView *)theTableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ if (_canEditRows) {
+ RKTableViewCellMapping *cellMapping = [self cellMappingForObjectAtIndexPath:indexPath];
+ UITableViewCell *cell = [self tableView:self.tableView cellForRowAtIndexPath:indexPath];
+ if (cellMapping.editingStyleForObjectAtIndexPath) {
+ RKLogTrace(@"Found a block for tableView:editingStyleForRowAtIndexPath: Executing...");
+ id object = [self objectForRowAtIndexPath:indexPath];
+ return cellMapping.editingStyleForObjectAtIndexPath(cell, object, indexPath);
+ }
+ return UITableViewCellEditingStyleDelete;
+ }
+ return UITableViewCellEditingStyleNone;
+}
+
+- (void)tableView:(UITableView *)theTableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ if ([self.delegate respondsToSelector:@selector(tableController:didEndEditing:atIndexPath:)]) {
+ id object = [self objectForRowAtIndexPath:indexPath];
+ [self.delegate tableController:self didEndEditing:object atIndexPath:indexPath];
+ }
+}
+
+- (void)tableView:(UITableView *)theTableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ if ([self.delegate respondsToSelector:@selector(tableController:willBeginEditing:atIndexPath:)]) {
+ id object = [self objectForRowAtIndexPath:indexPath];
+ [self.delegate tableController:self willBeginEditing:object atIndexPath:indexPath];
+ }
+}
+
+- (NSIndexPath *)tableView:(UITableView *)theTableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath
+{
+ if (_canMoveRows) {
+ RKTableViewCellMapping *cellMapping = [self cellMappingForObjectAtIndexPath:sourceIndexPath];
+ if (cellMapping.targetIndexPathForMove) {
+ RKLogTrace(@"Found a block for tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath: Executing...");
+ UITableViewCell *cell = [self tableView:self.tableView cellForRowAtIndexPath:sourceIndexPath];
+ id object = [self objectForRowAtIndexPath:sourceIndexPath];
+ return cellMapping.targetIndexPathForMove(cell, object, sourceIndexPath, proposedDestinationIndexPath);
+ }
+ }
+ return proposedDestinationIndexPath;
+}
+
+- (NSIndexPath *)tableView:(UITableView *)theTableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ [self removeSwipeView:YES];
+ return indexPath;
+}
+
+#pragma mark - Network Table Loading
+
+- (void)cancelLoad
+{
+ [self.objectRequestOperation cancel];
+}
+
+- (NSDate *)lastUpdatedDate
+{
+ if (! self.objectRequestOperation) {
+ return nil;
+ }
+
+ if (_autoRefreshFromNetwork) {
+// NSAssert(_cache, @"Found a nil cache when trying to read our last loaded time");
+// NSDictionary *lastUpdatedDates = [_cache dictionaryForCacheKey:lastUpdatedDateDictionaryKey];
+// RKLogTrace(@"Last updated dates dictionary retrieved from tableController cache: %@", lastUpdatedDates);
+// if (lastUpdatedDates) {
+// NSString *absoluteURLString = [self.requestOperation.request.URL absoluteString];
+// NSNumber *lastUpdatedTimeIntervalSince1970 = (NSNumber *)[lastUpdatedDates objectForKey:absoluteURLString];
+// if (absoluteURLString && lastUpdatedTimeIntervalSince1970) {
+// return [NSDate dateWithTimeIntervalSince1970:[lastUpdatedTimeIntervalSince1970 doubleValue]];
+// }
+// }
+ }
+ return nil;
+}
+
+- (BOOL)isAutoRefreshNeeded
+{
+ BOOL isAutoRefreshNeeded = NO;
+ if (_autoRefreshFromNetwork) {
+ isAutoRefreshNeeded = YES;
+ NSDate *lastUpdatedDate = [self lastUpdatedDate];
+ RKLogTrace(@"Last updated: %@", lastUpdatedDate);
+ if (lastUpdatedDate) {
+ RKLogTrace(@"-timeIntervalSinceNow=%f, autoRefreshRate=%f",
+ -[lastUpdatedDate timeIntervalSinceNow], _autoRefreshRate);
+ isAutoRefreshNeeded = (-[lastUpdatedDate timeIntervalSinceNow] > _autoRefreshRate);
+ }
+ }
+ return isAutoRefreshNeeded;
+}
+
+#pragma mark - RKRequestDelegate & RKObjectLoaderDelegate methods
+
+//- (void)requestDidStartLoad:(RKRequest *)request
+//{
+// RKLogTrace(@"tableController %@ started loading.", self);
+// [self didStartLoad];
+//}
+//
+//- (void)requestDidCancelLoad:(RKRequest *)request
+//{
+// RKLogTrace(@"tableController %@ cancelled loading.", self);
+// self.loading = NO;
+//
+// if ([self.delegate respondsToSelector:@selector(tableControllerDidCancelLoad:)]) {
+// [self.delegate tableControllerDidCancelLoad:self];
+// }
+//}
+//
+//- (void)requestDidTimeout:(RKRequest *)request
+//{
+// RKLogTrace(@"tableController %@ timed out while loading.", self);
+// self.loading = NO;
+//}
+//
+//- (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response
+//{
+// RKLogTrace(@"tableController %@ finished loading.", self);
+//
+// // Updated the lastUpdatedDate dictionary using the URL of the request
+// if (self.autoRefreshFromNetwork) {
+// NSAssert(_cache, @"Found a nil cache when trying to save our last loaded time");
+// NSMutableDictionary *lastUpdatedDates = [[_cache dictionaryForCacheKey:lastUpdatedDateDictionaryKey] mutableCopy];
+// if (lastUpdatedDates) {
+// [_cache invalidateEntry:lastUpdatedDateDictionaryKey];
+// } else {
+// lastUpdatedDates = [[NSMutableDictionary alloc] init];
+// }
+// NSNumber *timeIntervalSince1970 = [NSNumber numberWithDouble:[[NSDate date] timeIntervalSince1970]];
+// RKLogTrace(@"Setting timeIntervalSince1970=%@ for URL %@", timeIntervalSince1970, [request.URL absoluteString]);
+// [lastUpdatedDates setObject:timeIntervalSince1970
+// forKey:[request.URL absoluteString]];
+// [_cache writeDictionary:lastUpdatedDates withCacheKey:lastUpdatedDateDictionaryKey];
+// [lastUpdatedDates release];
+// }
+//}
+//
+//- (void)objectLoader:(RKObjectLoader *)objectLoader didFailWithError:(NSError *)error
+//{
+// RKLogError(@"tableController %@ failed network load with error: %@", self, error);
+// [self didFailLoadWithError:error];
+//}
+//
+//- (void)objectLoaderDidFinishLoading:(RKObjectLoader *)objectLoader
+//{
+// if ([self.delegate respondsToSelector:@selector(tableController:didLoadTableWithObjectLoader:)]) {
+// [self.delegate tableController:self didLoadTableWithObjectLoader:objectLoader];
+// }
+//
+// [objectLoader reset];
+// [self didFinishLoad];
+//}
+
+- (void)didStartLoad
+{
+ self.loading = YES;
+}
+
+- (void)didFailLoadWithError:(NSError *)error
+{
+ self.error = error;
+ [self didFinishLoad];
+}
+
+- (void)didFinishLoad
+{
+ self.empty = [self isConsideredEmpty];
+ self.loading = [self.objectRequestOperation isExecuting]; // Mutate loading state after we have adjusted empty
+ self.loaded = YES;
+
+ if (![self isEmpty] && ![self isLoading]) {
+ [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadObjectsNotification object:self];
+ }
+
+ if (self.delegate && [_delegate respondsToSelector:@selector(tableControllerDidFinalizeLoad:)]) {
+ [self.delegate performSelector:@selector(tableControllerDidFinalizeLoad:) withObject:self];
+ }
+}
+
+#pragma mark - Table Overlay Views
+
+- (UIImage *)imageForState:(RKTableControllerState)state
+{
+ switch (state) {
+ case RKTableControllerStateNormal:
+ case RKTableControllerStateLoading:
+ case RKTableControllerStateNotYetLoaded:
+ break;
+
+ case RKTableControllerStateEmpty:
+ return self.imageForEmpty;
+ break;
+
+ case RKTableControllerStateError:
+ return self.imageForError;
+ break;
+
+ case RKTableControllerStateOffline:
+ return self.imageForOffline;
+ break;
+
+ default:
+ break;
+ }
+
+ return nil;
+}
+
+- (UIImage *)overlayImage
+{
+ return _stateOverlayImageView.image;
+}
+
+// Adds an overlay view above the table
+- (void)addToOverlayView:(UIView *)view modally:(BOOL)modally
+{
+ if (! _tableOverlayView) {
+ CGRect overlayFrame = CGRectIsEmpty(self.overlayFrame) ? self.tableView.frame : self.overlayFrame;
+ _tableOverlayView = [[UIView alloc] initWithFrame:overlayFrame];
+ _tableOverlayView.autoresizesSubviews = YES;
+ _tableOverlayView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleBottomMargin;
+ NSInteger tableIndex = [_tableView.superview.subviews indexOfObject:_tableView];
+ if (tableIndex != NSNotFound) {
+ [_tableView.superview addSubview:_tableOverlayView];
+ }
+ }
+
+ // When modal, we enable user interaction to catch & discard events on the overlay and its subviews
+ _tableOverlayView.userInteractionEnabled = modally;
+ view.userInteractionEnabled = modally;
+
+ if (CGRectIsEmpty(view.frame)) {
+ view.frame = _tableOverlayView.bounds;
+
+ // Center it in the overlay
+ view.center = _tableOverlayView.center;
+ }
+
+ [_tableOverlayView addSubview:view];
+}
+
+- (void)resetOverlayView
+{
+ if (_stateOverlayImageView && _stateOverlayImageView.image == nil) {
+ [_stateOverlayImageView removeFromSuperview];
+ }
+ if (_tableOverlayView && _tableOverlayView.subviews.count == 0) {
+ [_tableOverlayView removeFromSuperview];
+ _tableOverlayView = nil;
+ }
+}
+
+- (void)addSubviewOverTableView:(UIView *)view
+{
+ NSInteger tableIndex = [_tableView.superview.subviews
+ indexOfObject:_tableView];
+ if (NSNotFound != tableIndex) {
+ [_tableView.superview addSubview:view];
+ }
+}
+
+- (BOOL)removeImageFromOverlay:(UIImage *)image
+{
+ if (image && _stateOverlayImageView.image == image) {
+ _stateOverlayImageView.image = nil;
+ return YES;
+ }
+ return NO;
+}
+
+- (void)showImageInOverlay:(UIImage *)image
+{
+ NSAssert(self.tableView, @"Cannot add an overlay image to a nil tableView");
+ if (! _stateOverlayImageView) {
+ _stateOverlayImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
+ _stateOverlayImageView.opaque = YES;
+ _stateOverlayImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleBottomMargin;
+ _stateOverlayImageView.contentMode = UIViewContentModeCenter;
+ }
+ _stateOverlayImageView.image = image;
+ [self addToOverlayView:_stateOverlayImageView modally:self.showsOverlayImagesModally];
+}
+
+- (void)removeImageOverlay
+{
+ _stateOverlayImageView.image = nil;
+ [_stateOverlayImageView removeFromSuperview];
+ [self resetOverlayView];
+}
+
+- (void)setImageForEmpty:(UIImage *)imageForEmpty
+{
+ BOOL imageRemoved = [self removeImageFromOverlay:_imageForEmpty];
+ _imageForEmpty = imageForEmpty;
+ if (imageRemoved) [self showImageInOverlay:_imageForEmpty];
+}
+
+- (void)setImageForError:(UIImage *)imageForError
+{
+ BOOL imageRemoved = [self removeImageFromOverlay:_imageForError];
+ _imageForError = imageForError;
+ if (imageRemoved) [self showImageInOverlay:_imageForError];
+}
+
+- (void)setImageForOffline:(UIImage *)imageForOffline
+{
+ BOOL imageRemoved = [self removeImageFromOverlay:_imageForOffline];
+ _imageForOffline = imageForOffline;
+ if (imageRemoved) [self showImageInOverlay:_imageForOffline];
+}
+
+- (void)setLoadingView:(UIView *)loadingView
+{
+ BOOL viewRemoved = (_loadingView.superview != nil);
+ [_loadingView removeFromSuperview];
+ [self resetOverlayView];
+ _loadingView = loadingView;
+ if (viewRemoved) [self addToOverlayView:_loadingView modally:NO];
+}
+
+#pragma mark - KVO & Table States
+
+- (BOOL)isLoading
+{
+ return (self.state & RKTableControllerStateLoading) != 0;
+}
+
+- (BOOL)isLoaded
+{
+ return (self.state & RKTableControllerStateNotYetLoaded) == 0;
+}
+
+- (BOOL)isOffline
+{
+ return (self.state & RKTableControllerStateOffline) != 0;
+}
+- (BOOL)isOnline
+{
+ return ![self isOffline];
+}
+
+- (BOOL)isError
+{
+ return (self.state & RKTableControllerStateError) != 0;
+}
+
+- (BOOL)isEmpty
+{
+ return (self.state & RKTableControllerStateEmpty) != 0;
+}
+
+- (void)isLoadingDidChange
+{
+ if ([self isLoading]) {
+ if ([self.delegate respondsToSelector:@selector(tableControllerDidStartLoad:)]) {
+ [self.delegate tableControllerDidStartLoad:self];
+ }
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidStartLoadNotification object:self];
+
+ if (self.loadingView) {
+ [self addToOverlayView:self.loadingView modally:NO];
+ }
+ } else {
+ if ([self.delegate respondsToSelector:@selector(tableControllerDidFinishLoad:)]) {
+ [self.delegate tableControllerDidFinishLoad:self];
+ }
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidFinishLoadNotification object:self];
+
+ if (self.loadingView) {
+ [self.loadingView removeFromSuperview];
+ [self resetOverlayView];
+ }
+
+ [self resetPullToRefreshRecognizer];
+ }
+
+ // We don't want any image overlays applied until loading is finished
+ _stateOverlayImageView.hidden = [self isLoading];
+}
+
+- (void)isLoadedDidChange
+{
+ if ([self isLoaded]) {
+ RKLogDebug(@"%@: is now loaded.", self);
+ } else {
+ RKLogDebug(@"%@: is NOT loaded.", self);
+ }
+}
+
+- (void)isErrorDidChange
+{
+ if ([self isError]) {
+ if ([self.delegate respondsToSelector:@selector(tableController:didFailLoadWithError:)]) {
+ [self.delegate tableController:self didFailLoadWithError:self.error];
+ }
+
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:self.error forKey:RKErrorNotificationErrorKey];
+ [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadErrorNotification object:self userInfo:userInfo];
+ }
+}
+
+- (void)isEmptyDidChange
+{
+ if ([self isEmpty]) {
+ if ([self.delegate respondsToSelector:@selector(tableControllerDidBecomeEmpty:)]) {
+ [self.delegate tableControllerDidBecomeEmpty:self];
+ }
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadEmptyNotification object:self];
+ }
+}
+
+- (void)isOnlineDidChange
+{
+ if ([self isOnline]) {
+ // We just transitioned to online
+ if ([self.delegate respondsToSelector:@selector(tableControllerDidBecomeOnline:)]) {
+ [self.delegate tableControllerDidBecomeOnline:self];
+ }
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidBecomeOnline object:self];
+ } else {
+ // We just transitioned to offline
+ if ([self.delegate respondsToSelector:@selector(tableControllerDidBecomeOffline:)]) {
+ [self.delegate tableControllerDidBecomeOffline:self];
+ }
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidBecomeOffline object:self];
+ }
+}
+
+- (void)updateTableViewForStateChange:(NSDictionary *)change
+{
+ RKTableControllerState oldState = [[change valueForKey:NSKeyValueChangeOldKey] integerValue];
+ RKTableControllerState newState = [[change valueForKey:NSKeyValueChangeNewKey] integerValue];
+
+ // Determine state transitions
+ BOOL loadedChanged = ((oldState ^ newState) & RKTableControllerStateNotYetLoaded);
+ BOOL emptyChanged = ((oldState ^ newState) & RKTableControllerStateEmpty);
+ BOOL offlineChanged = ((oldState ^ newState) & RKTableControllerStateOffline);
+ BOOL loadingChanged = ((oldState ^ newState) & RKTableControllerStateLoading);
+ BOOL errorChanged = ((oldState ^ newState) & RKTableControllerStateError);
+
+ if (loadedChanged) [self isLoadedDidChange];
+ if (emptyChanged) [self isEmptyDidChange];
+ if (offlineChanged) [self isOnlineDidChange];
+ if (errorChanged) [self isErrorDidChange];
+ if (loadingChanged) [self isLoadingDidChange];
+
+ // Clear the image from the overlay
+ _stateOverlayImageView.image = nil;
+
+ // Determine the appropriate overlay image to display (if any)
+ if (self.state == RKTableControllerStateNormal) {
+ [self removeImageOverlay];
+ } else {
+ if ([self isLoading]) {
+ // During a load we don't adjust the overlay
+ return;
+ }
+
+ // Though the table can be in more than one state, we only
+ // want to display a single overlay image.
+ if ([self isOffline] && self.imageForOffline) {
+ [self showImageInOverlay:self.imageForOffline];
+ } else if ([self isError] && self.imageForError) {
+ [self showImageInOverlay:self.imageForError];
+ } else if ([self isEmpty] && self.imageForEmpty) {
+ [self showImageInOverlay:self.imageForEmpty];
+ }
+ }
+
+ // Remove the overlay if no longer in use
+ [self resetOverlayView];
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
+{
+ if ([keyPath isEqualToString:@"state"]) {
+ [self updateTableViewForStateChange:change];
+ } else if ([keyPath isEqualToString:@"error"]) {
+ [self setErrorState:(self.error != nil)];
+ }
+}
+
+#pragma mark - Pull to Refresh
+
+- (RKRefreshGestureRecognizer *)pullToRefreshGestureRecognizer
+{
+ RKRefreshGestureRecognizer *refreshRecognizer = nil;
+ for (RKRefreshGestureRecognizer *recognizer in self.tableView.gestureRecognizers) {
+ if ([recognizer isKindOfClass:[RKRefreshGestureRecognizer class]]) {
+ refreshRecognizer = recognizer;
+ break;
+ }
+ }
+ return refreshRecognizer;
+}
+
+- (void)setPullToRefreshEnabled:(BOOL)pullToRefreshEnabled
+{
+ RKRefreshGestureRecognizer *recognizer = nil;
+ if (pullToRefreshEnabled) {
+ recognizer = [[RKRefreshGestureRecognizer alloc] initWithTarget:self action:@selector(pullToRefreshStateChanged:)];
+ [self.tableView addGestureRecognizer:recognizer];
+ }
+ else {
+ recognizer = [self pullToRefreshGestureRecognizer];
+ if (recognizer)
+ [self.tableView removeGestureRecognizer:recognizer];
+ }
+ _pullToRefreshEnabled = pullToRefreshEnabled;
+}
+
+- (void)pullToRefreshStateChanged:(UIGestureRecognizer *)gesture
+{
+ // Migrated to subclass...
+// if (gesture.state == UIGestureRecognizerStateRecognized) {
+// if ([self pullToRefreshDataSourceIsLoading:gesture]) return;
+// RKLogDebug(@"%@: pull to refresh triggered from gesture: %@", self, gesture);
+// [self loadTableWithRequest:self.request];
+// }
+}
+
+- (void)resetPullToRefreshRecognizer
+{
+ RKRefreshGestureRecognizer *recognizer = [self pullToRefreshGestureRecognizer];
+ if (recognizer)
+ [recognizer setRefreshState:RKRefreshIdle];
+}
+
+- (BOOL)pullToRefreshDataSourceIsLoading:(UIGestureRecognizer *)gesture
+{
+ // If we have already been loaded and we are loading again, a refresh is taking place...
+ return [self isLoaded] && [self isLoading] && [self isOnline];
+}
+
+- (NSDate *)pullToRefreshDataSourceLastUpdated:(UIGestureRecognizer *)gesture
+{
+ NSDate *dataSourceLastUpdated = [self lastUpdatedDate];
+ return dataSourceLastUpdated ? dataSourceLastUpdated : [NSDate date];
+}
+
+#pragma mark - Cell Swipe Menu Methods
+
+- (void)setupSwipeGestureRecognizers
+{
+ // Setup a right swipe gesture recognizer
+ UISwipeGestureRecognizer *rightSwipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRight:)];
+ rightSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionRight;
+ [self.tableView addGestureRecognizer:rightSwipeGestureRecognizer];
+
+ // Setup a left swipe gesture recognizer
+ UISwipeGestureRecognizer *leftSwipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeft:)];
+ leftSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionLeft;
+ [self.tableView addGestureRecognizer:leftSwipeGestureRecognizer];
+}
+
+- (void)removeSwipeGestureRecognizers
+{
+ for (UIGestureRecognizer *recognizer in self.tableView.gestureRecognizers) {
+ if ([recognizer isKindOfClass:[UISwipeGestureRecognizer class]]) {
+ [self.tableView removeGestureRecognizer:recognizer];
+ }
+ }
+}
+
+- (void)setCanEditRows:(BOOL)canEditRows
+{
+ NSAssert(!_cellSwipeViewsEnabled, @"Table model cannot be made editable when cell swipe menus are enabled");
+ _canEditRows = canEditRows;
+}
+
+- (void)setCellSwipeViewsEnabled:(BOOL)cellSwipeViewsEnabled
+{
+ NSAssert(!_canEditRows, @"Cell swipe menus cannot be enabled for editable tableModels");
+ if (cellSwipeViewsEnabled) {
+ [self setupSwipeGestureRecognizers];
+ } else {
+ [self removeSwipeView:YES];
+ [self removeSwipeGestureRecognizers];
+ }
+ _cellSwipeViewsEnabled = cellSwipeViewsEnabled;
+}
+
+- (void)swipe:(UISwipeGestureRecognizer *)recognizer direction:(UISwipeGestureRecognizerDirection)direction
+{
+ if (_cellSwipeViewsEnabled && recognizer && recognizer.state == UIGestureRecognizerStateEnded) {
+ CGPoint location = [recognizer locationInView:self.tableView];
+ NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location];
+ UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
+ id object = [self objectForRowAtIndexPath:indexPath];
+
+ if (cell.frame.origin.x != 0) {
+ [self removeSwipeView:YES];
+ return;
+ }
+
+ [self removeSwipeView:NO];
+
+ if (cell != _swipeCell && !_animatingCellSwipe) {
+ [self addSwipeViewTo:cell withObject:object direction:direction];
+ }
+ }
+}
+
+- (void)swipeLeft:(UISwipeGestureRecognizer *)recognizer
+{
+ [self swipe:recognizer direction:UISwipeGestureRecognizerDirectionLeft];
+}
+
+- (void)swipeRight:(UISwipeGestureRecognizer *)recognizer
+{
+ [self swipe:recognizer direction:UISwipeGestureRecognizerDirectionRight];
+}
+
+- (void)addSwipeViewTo:(UITableViewCell *)cell withObject:(id)object direction:(UISwipeGestureRecognizerDirection)direction
+{
+ if (_cellSwipeViewsEnabled) {
+ NSAssert(cell, @"Cannot process swipe view with nil cell");
+ NSAssert(object, @"Cannot process swipe view with nil object");
+
+ _cellSwipeView.frame = cell.frame;
+
+ if ([self.delegate respondsToSelector:@selector(tableController:willAddSwipeView:toCell:forObject:)]) {
+ [self.delegate tableController:self
+ willAddSwipeView:_cellSwipeView
+ toCell:cell
+ forObject:object];
+ }
+
+ [self.tableView insertSubview:_cellSwipeView belowSubview:cell];
+
+ _swipeCell = cell;
+ _swipeObject = object;
+ _swipeDirection = direction;
+
+ CGRect cellFrame = cell.frame;
+
+ _cellSwipeView.frame = CGRectMake(0, cellFrame.origin.y, cellFrame.size.width, cellFrame.size.height);
+
+ _animatingCellSwipe = YES;
+ [UIView beginAnimations:nil context:nil];
+ [UIView setAnimationDuration:0.2];
+ [UIView setAnimationDelegate:self];
+ [UIView setAnimationDidStopSelector:@selector(animationDidStopAddingSwipeView:finished:context:)];
+
+ cell.frame = CGRectMake(direction == UISwipeGestureRecognizerDirectionRight ? cellFrame.size.width : -cellFrame.size.width, cellFrame.origin.y, cellFrame.size.width, cellFrame.size.height);
+ [UIView commitAnimations];
+ }
+}
+
+- (void)animationDidStopAddingSwipeView:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
+{
+ _animatingCellSwipe = NO;
+}
+
+- (void)removeSwipeView:(BOOL)animated
+{
+ if (!_cellSwipeViewsEnabled || !_swipeCell || _animatingCellSwipe) {
+ RKLogTrace(@"Exiting early with _cellSwipeViewsEnabled=%d, _swipCell=%@, _animatingCellSwipe=%d",
+ _cellSwipeViewsEnabled, _swipeCell, _animatingCellSwipe);
+ return;
+ }
+
+ if ([self.delegate respondsToSelector:@selector(tableController:willRemoveSwipeView:fromCell:forObject:)]) {
+ [self.delegate tableController:self
+ willRemoveSwipeView:_cellSwipeView
+ fromCell:_swipeCell
+ forObject:_swipeObject];
+ }
+
+ if (animated) {
+ [UIView beginAnimations:nil context:nil];
+ [UIView setAnimationDuration:0.2];
+ if (_swipeDirection == UISwipeGestureRecognizerDirectionRight) {
+ _swipeCell.frame = CGRectMake(BOUNCE_PIXELS, _swipeCell.frame.origin.y, _swipeCell.frame.size.width, _swipeCell.frame.size.height);
+ } else {
+ _swipeCell.frame = CGRectMake(-BOUNCE_PIXELS, _swipeCell.frame.origin.y, _swipeCell.frame.size.width, _swipeCell.frame.size.height);
+ }
+ _animatingCellSwipe = YES;
+ [UIView setAnimationDelegate:self];
+ [UIView setAnimationDidStopSelector:@selector(animationDidStopOne:finished:context:)];
+ [UIView commitAnimations];
+ } else {
+ [_cellSwipeView removeFromSuperview];
+ _swipeCell.frame = CGRectMake(0, _swipeCell.frame.origin.y, _swipeCell.frame.size.width, _swipeCell.frame.size.height);
+ _swipeCell = nil;
+ }
+}
+
+- (void)animationDidStopOne:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
+{
+ [UIView beginAnimations:nil context:nil];
+ [UIView setAnimationDuration:0.2];
+ if (_swipeDirection == UISwipeGestureRecognizerDirectionRight) {
+ _swipeCell.frame = CGRectMake(BOUNCE_PIXELS*2, _swipeCell.frame.origin.y, _swipeCell.frame.size.width, _swipeCell.frame.size.height);
+ } else {
+ _swipeCell.frame = CGRectMake(-BOUNCE_PIXELS*2, _swipeCell.frame.origin.y, _swipeCell.frame.size.width, _swipeCell.frame.size.height);
+ }
+ [UIView setAnimationDelegate:self];
+ [UIView setAnimationDidStopSelector:@selector(animationDidStopTwo:finished:context:)];
+ [UIView setAnimationCurve:UIViewAnimationCurveLinear];
+ [UIView commitAnimations];
+}
+
+- (void)animationDidStopTwo:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
+{
+ [UIView commitAnimations];
+ [UIView beginAnimations:nil context:nil];
+ [UIView setAnimationDuration:0.2];
+ if (_swipeDirection == UISwipeGestureRecognizerDirectionRight) {
+ _swipeCell.frame = CGRectMake(0, _swipeCell.frame.origin.y, _swipeCell.frame.size.width, _swipeCell.frame.size.height);
+ } else {
+ _swipeCell.frame = CGRectMake(0, _swipeCell.frame.origin.y, _swipeCell.frame.size.width, _swipeCell.frame.size.height);
+ }
+ [UIView setAnimationDelegate:self];
+ [UIView setAnimationDidStopSelector:@selector(animationDidStopThree:finished:context:)];
+ [UIView setAnimationCurve:UIViewAnimationCurveLinear];
+ [UIView commitAnimations];
+}
+
+- (void)animationDidStopThree:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
+{
+ _animatingCellSwipe = NO;
+ _swipeCell = nil;
+ [_cellSwipeView removeFromSuperview];
+}
+
+#pragma mark UIScrollViewDelegate methods
+
+- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
+{
+ [self removeSwipeView:YES];
+}
+
+- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
+{
+ [self removeSwipeView:NO];
+ return YES;
+}
+
+- (void)reloadRowForObject:(id)object withRowAnimation:(UITableViewRowAnimation)rowAnimation
+{
+ NSIndexPath *indexPath = [self indexPathForObject:object];
+ if (indexPath) {
+ [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:rowAnimation];
+ }
+}
+
+@end
79 Code/RKAbstractTableController_Internals.h
View
@@ -0,0 +1,79 @@
+//
+// RKAbstractTableController_Internals.h
+// RestKit
+//
+// Created by Jeff Arena on 8/11/11.
+// Copyright (c) 2009-2012 RestKit. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import <UIKit/UIKit.h>
+#import "RKRefreshGestureRecognizer.h"
+#import "RKObjectRequestOperation.h"
+
+/*
+ A private continuation class for subclass implementations of RKAbstractTableController
+ */
+@interface RKAbstractTableController () <RKRefreshTriggerProtocol>
+
+@property (weak, nonatomic, readwrite, assign) UITableView *tableView;
+@property (weak, nonatomic, readwrite, assign) UIViewController *viewController;
+@property (nonatomic, assign, readwrite) RKTableControllerState state;
+@property (nonatomic, strong) NSURLRequest *request;
+@property (nonatomic, readwrite, strong) RKObjectRequestOperation *objectRequestOperation;
+@property (nonatomic, readwrite, strong) NSError *error;
+@property (nonatomic, readwrite, strong) NSMutableArray *headerItems;
+@property (nonatomic, readwrite, strong) NSMutableArray *footerItems;
+@property (nonatomic, readonly) UIView *tableOverlayView;
+@property (nonatomic, readonly) UIImageView *stateOverlayImageView;
+@property (nonatomic, strong) UIView *pullToRefreshHeaderView;
+
+#pragma mark - Subclass Load Event Hooks
+
+- (void)didStartLoad;
+
+/**
+ Must be invoked when the table controller has finished loading.
+
+ Responsible for finalizing loading, empty, and loaded states
+ and cleaning up the table overlay view.
+ */
+- (void)didFinishLoad;
+- (void)didFailLoadWithError:(NSError *)error;
+
+#pragma mark - Table View Overlay
+
+- (void)addToOverlayView:(UIView *)view modally:(BOOL)modally;
+- (void)resetOverlayView;
+- (void)addSubviewOverTableView:(UIView *)view;
+- (BOOL)removeImageFromOverlay:(UIImage *)image;
+- (void)showImageInOverlay:(UIImage *)image;
+- (void)removeImageOverlay;
+
+#pragma mark - Pull to Refresh Private Methods
+
+- (void)pullToRefreshStateChanged:(UIGestureRecognizer *)gesture;
+- (void)resetPullToRefreshRecognizer;
+
+/**
+ Returns a Boolean value indicating if the table controller
+ should be considered empty and transitioned into the empty state.
+ Used by the abstract table controller to trigger state transitions.
+
+ **NOTE**: This is an abstract method that MUST be implemented with
+ a subclass.
+ */
+- (BOOL)isConsideredEmpty;
+
+@end
68 Code/RKFetchedResultsTableController.h
View
@@ -0,0 +1,68 @@
+//
+// RKFetchedResultsTableController.h
+// RestKit
+//
+// Created by Blake Watters on 8/2/11.
+// Copyright (c) 2009-2012 RestKit. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import <CoreData/CoreData.h>
+#import "RKAbstractTableController.h"
+
+typedef UIView *(^RKFetchedResultsTableViewViewForHeaderInSectionBlock)(NSUInteger sectionIndex, NSString *sectionTitle);
+
+@class RKFetchedResultsTableController;
+@protocol RKFetchedResultsTableControllerDelegate <RKAbstractTableControllerDelegate>
+
+@optional
+
+// Sections
+- (void)tableController:(RKFetchedResultsTableController *)tableController didInsertSectionAtIndex:(NSUInteger)sectionIndex;
+- (void)tableController:(RKFetchedResultsTableController *)tableController didDeleteSectionAtIndex:(NSUInteger)sectionIndex;
+
+@end
+
+/**
+ Instances of RKFetchedResultsTableController provide an interface for driving a UITableView
+ */
+@interface RKFetchedResultsTableController : RKAbstractTableController <NSFetchedResultsControllerDelegate>
+
+// Delegate
+@property (nonatomic, weak) id<RKFetchedResultsTableControllerDelegate> delegate;
+
+// Fetched Results Controller
+@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;
+@property (nonatomic, strong) NSArray *fetchRequestBlocks; // An array of blocks for determining the fetch request for a URL
+@property (nonatomic, strong) NSURLRequest *request;
+@property (nonatomic, strong) NSFetchRequest *fetchRequest;
+@property (nonatomic, strong) NSPredicate *predicate;
+@property (nonatomic, strong) NSArray *sortDescriptors;
+@property (nonatomic, copy) NSString *sectionNameKeyPath;
+@property (nonatomic, copy) NSString *cacheName;
+@property (nonatomic, strong, readonly) NSFetchedResultsController *fetchedResultsController;
+
+// Configuring Headers and Sections
+@property (nonatomic, assign) CGFloat heightForHeaderInSection;
+@property (nonatomic, copy) RKFetchedResultsTableViewViewForHeaderInSectionBlock onViewForHeaderInSection;
+@property (nonatomic, assign) BOOL showsSectionIndexTitles;
+
+// Sorting
+@property (nonatomic, assign) SEL sortSelector;
+@property (nonatomic, copy) NSComparator sortComparator;
+
+//- (void)setObjectMappingForClass:(Class)objectClass; // TODO: Kill this API... mapping descriptors will cover use case.
+- (void)loadTable;
+//- (void)loadTableWithFetchRequest:(NSFetchRequest *)fetchRequest;
+@end
675 Code/RKFetchedResultsTableController.m
View
@@ -0,0 +1,675 @@
+//
+// RKFetchedResultsTableController.m
+// RestKit
+//
+// Created by Blake Watters on 8/2/11.
+// Copyright (c) 2009-2012 RestKit. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import "RKFetchedResultsTableController.h"
+#import "RKAbstractTableController_Internals.h"
+#import "RKManagedObjectStore.h"
+#import "RKMappingOperation.h"
+#import "RKEntityMapping.h"
+#import "RKLog.h"
+#import "RKManagedObjectRequestOperation.h"
+
+// Define logging component
+#undef RKLogComponent
+#define RKLogComponent lcl_cRestKitUI
+
+@interface RKFetchedResultsTableController ()
+
+@property (nonatomic, assign) BOOL isEmptyBeforeAnimation;
+@property (nonatomic, strong, readwrite) NSFetchedResultsController *fetchedResultsController;
+@property (nonatomic, strong) NSArray *arraySortedFetchedObjects;
+
+- (BOOL)performFetch:(NSError **)error;
+- (void)updateSortedArray;
+@end
+
+@implementation RKFetchedResultsTableController
+
+@dynamic delegate;
+
+- (void)dealloc
+{
+ _fetchedResultsController.delegate = nil;
+ _sectionNameKeyPath = nil;
+}
+
+#pragma mark - Helpers
+
+- (BOOL)performFetch:(NSError **)error
+{
+ NSAssert(self.fetchedResultsController, @"Cannot perform a fetch: self.fetchedResultsController is nil.");
+
+ [NSFetchedResultsController deleteCacheWithName:self.fetchedResultsController.cacheName];
+ BOOL success = [self.fetchedResultsController performFetch:error];
+ if (!success) {
+ RKLogError(@"performFetch failed with error: %@", [*error localizedDescription]);
+ return NO;
+ } else {
+ RKLogTrace(@"performFetch completed successfully");
+ for (NSUInteger index = 0; index < [self sectionCount]; index++) {
+ if ([self.delegate respondsToSelector:@selector(tableController:didInsertSectionAtIndex:)]) {
+ [self.delegate tableController:self didInsertSectionAtIndex:index];
+ }
+
+ if ([self.delegate respondsToSelector:@selector(tableController:didInsertObject:atIndexPath:)]) {
+ for (NSUInteger row = 0; row < [self numberOfRowsInSection:index]; row++) {
+ NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:index];
+ id object = [self objectForRowAtIndexPath:indexPath];
+ [self.delegate tableController:self didInsertObject:object atIndexPath:indexPath];
+ }
+ }
+ }
+ }
+
+ return YES;
+}
+
+- (void)updateSortedArray
+{
+ self.arraySortedFetchedObjects = nil;
+
+ if (self.sortSelector || self.sortComparator) {
+ if (self.sortSelector) {
+ self.arraySortedFetchedObjects = [self.fetchedResultsController.fetchedObjects sortedArrayUsingSelector:self.sortSelector];
+ } else if (self.sortComparator) {
+ self.arraySortedFetchedObjects = [self.fetchedResultsController.fetchedObjects sortedArrayUsingComparator:self.sortComparator];
+ }
+
+ NSAssert(self.arraySortedFetchedObjects.count == self.fetchedResultsController.fetchedObjects.count,
+ @"sortSelector or sortComparator sort resulted in fewer objects than expected");
+ }
+}
+
+- (NSUInteger)headerSectionIndex
+{
+ return 0;
+}
+
+- (BOOL)isHeaderSection:(NSUInteger)section
+{
+ return (section == [self headerSectionIndex]);
+}
+
+- (BOOL)isHeaderRow:(NSUInteger)row
+{
+ BOOL isHeaderRow = NO;
+ NSUInteger headerItemCount = [self.headerItems count];
+ if ([self isEmpty] && self.emptyItem) {
+ isHeaderRow = (row > 0 && row <= headerItemCount);
+ } else {
+ isHeaderRow = (row < headerItemCount);
+ }
+ return isHeaderRow;
+}
+
+- (NSUInteger)footerSectionIndex
+{
+ return ([self sectionCount] - 1);
+}
+
+- (BOOL)isFooterSection:(NSUInteger)section
+{
+ return (section == [self footerSectionIndex]);
+}
+
+- (BOOL)isFooterRow:(NSUInteger)row
+{
+ NSUInteger sectionIndex = ([self sectionCount] - 1);
+ id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:sectionIndex];
+ NSUInteger firstFooterIndex = [sectionInfo numberOfObjects];
+ if (sectionIndex == 0) {
+ firstFooterIndex += (![self isEmpty] || self.showsHeaderRowsWhenEmpty) ? [self.headerItems count] : 0;
+ firstFooterIndex += ([self isEmpty] && self.emptyItem) ? 1 : 0;
+ }
+
+ return row >= firstFooterIndex;
+}
+
+- (BOOL)isHeaderIndexPath:(NSIndexPath *)indexPath
+{
+ return ((! [self isEmpty] || self.showsHeaderRowsWhenEmpty) &&
+ [self.headerItems count] > 0 &&
+ [self isHeaderSection:indexPath.section] &&
+ [self isHeaderRow:indexPath.row]);
+}
+
+- (BOOL)isFooterIndexPath:(NSIndexPath *)indexPath
+{
+ return ((! [self isEmpty] || self.showsFooterRowsWhenEmpty) &&
+ [self.footerItems count] > 0 &&
+ [self isFooterSection:indexPath.section] &&
+ [self isFooterRow:indexPath.row]);
+}
+
+- (BOOL)isEmptySection:(NSUInteger)section
+{
+ return (section == 0);
+}
+
+- (BOOL)isEmptyRow:(NSUInteger)row
+{
+ return (row == 0);
+}
+
+- (BOOL)isEmptyItemIndexPath:(NSIndexPath *)indexPath
+{
+ return ([self isEmpty] && self.emptyItem &&
+ [self isEmptySection:indexPath.section] &&
+ [self isEmptyRow:indexPath.row]);
+}
+
+- (NSIndexPath *)emptyItemIndexPath
+{
+ return [NSIndexPath indexPathForRow:0 inSection:0];
+}
+
+- (NSIndexPath *)fetchedResultsIndexPathForIndexPath:(NSIndexPath *)indexPath
+{
+ if (([self isEmpty] && self.emptyItem &&
+ [self isEmptySection:indexPath.section] &&
+ ! [self isEmptyRow:indexPath.row]) ||
+ ((! [self isEmpty] || self.showsHeaderRowsWhenEmpty) &&
+ [self.headerItems count] > 0 &&
+ [self isHeaderSection:indexPath.section] &&
+ ! [self isHeaderRow:indexPath.row])) {
+ NSUInteger adjustedRowIndex = indexPath.row;
+ if (![self isEmpty] || self.showsHeaderRowsWhenEmpty) {
+ adjustedRowIndex -= [self.headerItems count];
+ }
+ adjustedRowIndex -= ([self isEmpty] && self.emptyItem) ? 1 : 0;
+ return [NSIndexPath indexPathForRow:adjustedRowIndex
+ inSection:indexPath.section];
+ }
+ return indexPath;
+}
+
+- (NSIndexPath *)indexPathForFetchedResultsIndexPath:(NSIndexPath *)indexPath
+{
+ if (([self isEmpty] && self.emptyItem &&
+ [self isEmptySection:indexPath.section] &&
+ ! [self isEmptyRow:indexPath.row]) ||
+ ((! [self isEmpty] || self.showsHeaderRowsWhenEmpty) &&
+ [self.headerItems count] > 0 &&
+ [self isHeaderSection:indexPath.section])) {
+ NSUInteger adjustedRowIndex = indexPath.row;
+ if (![self isEmpty] || self.showsHeaderRowsWhenEmpty) {
+ adjustedRowIndex += [self.headerItems count];
+ }
+ adjustedRowIndex += ([self isEmpty] && self.emptyItem) ? 1 : 0;
+ return [NSIndexPath indexPathForRow:adjustedRowIndex
+ inSection:indexPath.section];
+ }
+ return indexPath;
+}
+
+#pragma mark - Public
+
+- (NSFetchRequest *)fetchRequest
+{
+ return _fetchRequest ? _fetchRequest : self.fetchedResultsController.fetchRequest;
+}
+
+- (void)loadTable
+{
+ NSAssert(self.fetchRequest || self.request, @"Cannot load a fetch results table without a request or a fetch request");
+ NSFetchRequest *fetchRequest = self.fetchRequest;
+ if (!self.fetchRequest) {
+ RKLogInfo(@"Determining fetch request from blocks for URL: '%@'", self.request.URL);
+ for (RKFetchRequestBlock fetchRequestBlock in self.fetchRequestBlocks) {
+ fetchRequest = fetchRequestBlock(self.request.URL);
+ if (fetchRequest) break;
+ }
+ }
+ NSAssert(fetchRequest, @"Failed to find a fetchRequest for URL: %@", self.request.URL);
+ self.fetchRequest = fetchRequest;
+
+ if (self.predicate) {
+ [fetchRequest setPredicate:self.predicate];
+ }
+ if (self.sortDescriptors) {
+ [fetchRequest setSortDescriptors:self.sortDescriptors];
+ }
+
+ RKLogTrace(@"Loading fetched results table view from managed object context %@ with fetch request: %@", self.managedObjectContext, fetchRequest);
+ NSFetchedResultsController *fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
+ managedObjectContext:self.managedObjectContext
+ sectionNameKeyPath:self.sectionNameKeyPath
+ cacheName:self.cacheName];
+ self.fetchedResultsController = fetchedResultsController;
+ self.fetchedResultsController.delegate = self;
+
+ // Perform the load
+ NSError *error;
+ [self didStartLoad];
+ BOOL success = [self performFetch:&error];
+ if (! success) {
+ [self didFailLoadWithError:error];
+ }
+ [self updateSortedArray];
+ [self didFinishLoad];
+
+ // Load the table view after we have finished the load to ensure the state
+ // is accurate when computing the table view data source responses
+ [self.tableView reloadData];
+
+ if ([self isAutoRefreshNeeded] && [self isOnline] &&
+ [self.objectRequestOperation isReady] ) { //&&
+// ![self.objectLoader.queue containsRequest:self.objectLoader]) {
+ [self performSelector:@selector(loadTableFromNetwork) withObject:nil afterDelay:0];
+ }
+}
+
+- (void)setSortSelector:(SEL)sortSelector
+{
+ NSAssert(self.sectionNameKeyPath == nil, @"Attempted to sort fetchedObjects across multiple sections");
+ NSAssert(self.sortComparator == nil, @"Attempted to sort fetchedObjects with a sortSelector when a sortComparator already exists");
+ _sortSelector = sortSelector;
+}
+
+- (void)setSortComparator:(NSComparator)sortComparator
+{
+ NSAssert(self.sectionNameKeyPath == nil, @"Attempted to sort fetchedObjects across multiple sections");
+ NSAssert(self.sortSelector == nil, @"Attempted to sort fetchedObjects with a sortComparator when a sortSelector already exists");
+ _sortComparator = sortComparator;
+}
+
+- (void)setSectionNameKeyPath:(NSString *)sectionNameKeyPath
+{
+ NSAssert(self.sortSelector == nil, @"Attempted to create a sectioned fetchedResultsController when a sortSelector is present");
+ NSAssert(self.sortComparator == nil, @"Attempted to create a sectioned fetchedResultsController when a sortComparator is present");
+ _sectionNameKeyPath = sectionNameKeyPath;
+}
+
+//- (void)setResourcePath:(NSString *)resourcePath
+//{
+// [_resourcePath release];
+// _resourcePath = [resourcePath copy];
+//
+// NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:self.objectManager.baseURL relativeToURL:resourcePath]];
+// self.objectRequestOperation = [self.objectManager objectRequestOperationWithRequest:request success:nil failure:nil];
+//}
+//
+//- (void)setObjectMappingForClass:(Class)objectClass
+//{
+//// NSParameterAssert(objectClass != NULL);
+//// NSAssert(self.objectLoader != NULL, @"Resource path (and thus object loader) must be set before setting object mapping.");
+//// NSAssert(self.objectManager != NULL, @"Object manager must exist before setting object mapping.");
+//// self.objectLoader.objectMapping = [self.objectManager.mappingProvider objectMappingForClass:objectClass];
+//}
+
+#pragma mark - Managing Sections
+
+- (NSUInteger)sectionCount
+{
+ return [[self.fetchedResultsController sections] count];
+}
+
+- (NSUInteger)rowCount
+{
+ NSUInteger fetchedItemCount = [[self.fetchedResultsController fetchedObjects] count];
+ NSUInteger nonFetchedItemCount = 0;
+ if (fetchedItemCount == 0) {
+ nonFetchedItemCount += self.emptyItem ? 1 : 0;
+ nonFetchedItemCount += self.showsHeaderRowsWhenEmpty ? [self.headerItems count] : 0;
+ nonFetchedItemCount += self.showsFooterRowsWhenEmpty ? [self.footerItems count] : 0;
+ } else {
+ nonFetchedItemCount += [self.headerItems count];
+ nonFetchedItemCount += [self.footerItems count];
+ }
+ return (fetchedItemCount + nonFetchedItemCount);
+}
+
+- (NSIndexPath *)indexPathForObject:(id)object
+{
+ if ([object isKindOfClass:[NSManagedObject class]]) {
+ return [self indexPathForFetchedResultsIndexPath:[self.fetchedResultsController indexPathForObject:object]];
+ } else if ([object isKindOfClass:[RKTableItem class]]) {
+ if ([object isEqual:self.emptyItem]) {
+ return ([self isEmpty]) ? [self emptyItemIndexPath] : nil;
+ } else if ([self.headerItems containsObject:object]) {
+ // Figure out the row number for the object
+ NSUInteger objectIndex = [self.headerItems indexOfObject:object];
+ NSUInteger row = ([self isEmpty] && self.emptyItem) ? (objectIndex + 1) : objectIndex;
+ return [NSIndexPath indexPathForRow:row inSection:[self headerSectionIndex]];
+ } else if ([self.footerItems containsObject:object]) {
+ NSUInteger footerSectionIndex = [self sectionCount] - 1;
+ id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:footerSectionIndex];
+ NSUInteger numberOfFetchedResults = sectionInfo.numberOfObjects;
+ NSUInteger objectIndex = [self.footerItems indexOfObject:object];
+ NSUInteger row = numberOfFetchedResults + objectIndex;
+ row += ([self isEmpty] && self.emptyItem) ? 1 : 0;
+ if ([self isHeaderSection:footerSectionIndex]) {
+ row += [self.headerItems count];
+ }
+
+ return [NSIndexPath indexPathForRow:row inSection:footerSectionIndex];
+ }
+ } else {
+ RKLogWarning(@"Asked for indexPath of unsupported object type '%@': %@", [object class], object);
+ }
+ return nil;
+}
+
+- (UITableViewCell *)cellForObject:(id)object
+{
+ NSIndexPath *indexPath = [self indexPathForObject:object];
+ NSAssert(indexPath, @"Failed to find indexPath for object: %@", object);
+ return [self.tableView cellForRowAtIndexPath:indexPath];
+}
+
+#pragma mark - UITableViewDataSource methods
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)theTableView
+{
+ NSAssert(theTableView == self.tableView, @"numberOfSectionsInTableView: invoked with inappropriate tableView: %@", theTableView);
+ RKLogTrace(@"numberOfSectionsInTableView: %d (%@)", [[self.fetchedResultsController sections] count], [[self.fetchedResultsController sections] valueForKey:@"name"]);
+ return [[self.fetchedResultsController sections] count];
+}
+
+- (NSInteger)tableView:(UITableView *)theTableView numberOfRowsInSection:(NSInteger)section
+{
+ NSAssert(theTableView == self.tableView, @"tableView:numberOfRowsInSection: invoked with inappropriate tableView: %@", theTableView);
+ RKLogTrace(@"%@ numberOfRowsInSection:%d = %d", self, section, self.sectionCount);
+ id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
+ NSUInteger numberOfRows = [sectionInfo numberOfObjects];
+
+ if ([self isHeaderSection:section]) {
+ numberOfRows += (![self isEmpty] || self.showsHeaderRowsWhenEmpty) ? [self.headerItems count] : 0;
+ numberOfRows += ([self isEmpty] && self.emptyItem) ? 1 : 0;
+ }
+
+ if ([self isFooterSection:section]) {
+ numberOfRows += (![self isEmpty] || self.showsFooterRowsWhenEmpty) ? [self.footerItems count] : 0;
+ }
+ return numberOfRows;
+}
+
+- (NSString *)tableView:(UITableView *)theTableView titleForHeaderInSection:(NSInteger)section
+{
+ id <NSFetchedResultsSectionInfo> sectionInfo = [[_fetchedResultsController sections] objectAtIndex:section];
+ return [sectionInfo name];
+}
+
+- (NSString *)tableView:(UITableView *)theTableView titleForFooterInSection:(NSInteger)section
+{
+ NSAssert(theTableView == self.tableView, @"tableView:titleForFooterInSection: invoked with inappropriate tableView: %@", theTableView);
+ return nil;
+}
+
+- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)theTableView
+{
+ if (theTableView.style == UITableViewStylePlain && self.showsSectionIndexTitles) {
+ return [_fetchedResultsController sectionIndexTitles];
+ }
+ return nil;
+}
+
+- (NSInteger)tableView:(UITableView *)theTableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
+{
+ if (theTableView.style == UITableViewStylePlain && self.showsSectionIndexTitles) {
+ return [self.fetchedResultsController sectionForSectionIndexTitle:title atIndex:index];
+ }
+ return 0;
+}
+
+//- (void)tableView:(UITableView *)theTableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
+//{
+// NSAssert(theTableView == self.tableView, @"tableView:commitEditingStyle:forRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView);
+// if (self.canEditRows && editingStyle == UITableViewCellEditingStyleDelete) {
+// NSManagedObject *managedObject = [self objectForRowAtIndexPath:indexPath];
+// RKObjectMapping *mapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForClass:[managedObject class]];
+// if ([mapping isKindOfClass:[RKEntityMapping class]]) {
+// RKEntityMapping *managedObjectMapping = (RKEntityMapping *)mapping;
+// NSString *primaryKeyAttribute = managedObjectMapping.primaryKeyAttribute;
+//
+// if ([managedObject valueForKeyPath:primaryKeyAttribute]) {
+// RKLogTrace(@"About to fire a delete request for managedObject: %@", managedObject);
+// [[RKObjectManager sharedManager] deleteObject:managedObject delegate:self];
+// } else {
+// RKLogTrace(@"About to locally delete managedObject: %@", managedObject);
+// NSManagedObjectContext *managedObjectContext = managedObject.managedObjectContext;
+// [managedObjectContext performBlock:^{
+// [managedObjectContext deleteObject:managedObject];
+//
+// NSError *error = nil;
+// [managedObjectContext save:&error];
+// if (error) {
+// RKLogError(@"Failed to save managedObjectContext after a delete with error: %@", error);
+// }
+// }];
+// }
+// }
+// }
+//}
+
+- (void)tableView:(UITableView *)theTableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destIndexPath
+{
+ NSAssert(theTableView == self.tableView, @"tableView:moveRowAtIndexPath:toIndexPath: invoked with inappropriate tableView: %@", theTableView);
+}
+
+- (BOOL)tableView:(UITableView *)theTableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ NSAssert(theTableView == self.tableView, @"tableView:canEditRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView);
+ return self.canEditRows && [self isOnline] && !([self isHeaderIndexPath:indexPath] || [self isFooterIndexPath:indexPath] || [self isEmptyItemIndexPath:indexPath]);
+}
+
+- (BOOL)tableView:(UITableView *)theTableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ NSAssert(theTableView == self.tableView, @"tableView:canMoveRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView);
+ return self.canMoveRows && !([self isHeaderIndexPath:indexPath] || [self isFooterIndexPath:indexPath] || [self isEmptyItemIndexPath:indexPath]);
+}
+
+#pragma mark - UITableViewDelegate methods
+
+- (CGFloat)tableView:(UITableView *)theTableView heightForHeaderInSection:(NSInteger)section
+{
+ NSAssert(theTableView == self.tableView, @"heightForHeaderInSection: invoked with inappropriate tableView: %@", theTableView);
+ return self.heightForHeaderInSection;
+}
+
+- (CGFloat)tableView:(UITableView *)theTableView heightForFooterInSection:(NSInteger)sectionIndex
+{
+ NSAssert(theTableView == self.tableView, @"heightForFooterInSection: invoked with inappropriate tableView: %@", theTableView);
+ return 0;
+}
+
+- (UIView *)tableView:(UITableView *)theTableView viewForHeaderInSection:(NSInteger)section
+{
+ NSAssert(theTableView == self.tableView, @"viewForHeaderInSection: invoked with inappropriate tableView: %@", theTableView);
+ if (self.onViewForHeaderInSection) {
+ NSString *sectionTitle = [self tableView:self.tableView titleForHeaderInSection:section];
+ if (sectionTitle) {
+ return self.onViewForHeaderInSection(section, sectionTitle);
+ }
+ }
+ return nil;
+}
+
+- (UIView *)tableView:(UITableView *)theTableView viewForFooterInSection:(NSInteger)sectionIndex
+{
+ NSAssert(theTableView == self.tableView, @"viewForFooterInSection: invoked with inappropriate tableView: %@", theTableView);
+ return nil;
+}
+
+#pragma mark - Cell Mappings
+
+- (id)objectForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ if ([self isEmptyItemIndexPath:indexPath]) {
+ return self.emptyItem;
+ } else if ([self isHeaderIndexPath:indexPath]) {
+ NSUInteger row = ([self isEmpty] && self.