mirror of
https://github.com/zhigang1992/RestKit.git
synced 2026-01-12 22:51:50 +08:00
1355 lines
56 KiB
Objective-C
Executable File
1355 lines
56 KiB
Objective-C
Executable File
//
|
|
// 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 "RKObjectMappingOperation.h"
|
|
#import "RKLog.h"
|
|
#import "RKErrors.h"
|
|
#import "RKReachabilityObserver.h"
|
|
#import "UIView+FindFirstResponder.h"
|
|
#import "RKRefreshGestureRecognizer.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";
|
|
|
|
@implementation RKAbstractTableController
|
|
|
|
@synthesize delegate = _delegate;
|
|
@synthesize viewController = _viewController;
|
|
@synthesize tableView = _tableView;
|
|
@synthesize sections = _sections;
|
|
@synthesize defaultRowAnimation = _defaultRowAnimation;
|
|
|
|
@synthesize objectLoader = _objectLoader;
|
|
@synthesize objectManager = _objectManager;
|
|
@synthesize cellMappings = _cellMappings;
|
|
@synthesize autoRefreshFromNetwork = _autoRefreshFromNetwork;
|
|
@synthesize autoRefreshRate = _autoRefreshRate;
|
|
|
|
@synthesize empty = _empty;
|
|
@synthesize loading = _loading;
|
|
@synthesize loaded = _loaded;
|
|
@synthesize online = _online;
|
|
@synthesize error = _error;
|
|
|
|
@synthesize imageForEmpty = _imageForEmpty;
|
|
@synthesize imageForError = _imageForError;
|
|
@synthesize imageForOffline = _imageForOffline;
|
|
@synthesize loadingView = _loadingView;
|
|
|
|
@synthesize variableHeightRows = _variableHeightRows;
|
|
@synthesize showsHeaderRowsWhenEmpty = _showsHeaderRowsWhenEmpty;
|
|
@synthesize showsFooterRowsWhenEmpty = _showsFooterRowsWhenEmpty;
|
|
@synthesize pullToRefreshEnabled = _pullToRefreshEnabled;
|
|
@synthesize headerItems = _headerItems;
|
|
@synthesize footerItems = _footerItems;
|
|
@synthesize canEditRows = _canEditRows;
|
|
@synthesize canMoveRows = _canMoveRows;
|
|
@synthesize autoResizesForKeyboard = _autoResizesForKeyboard;
|
|
@synthesize emptyItem = _emptyItem;
|
|
|
|
@synthesize cellSwipeViewsEnabled = _cellSwipeViewsEnabled;
|
|
@synthesize cellSwipeView = _cellSwipeView;
|
|
@synthesize swipeCell = _swipeCell;
|
|
@synthesize animatingCellSwipe = _animatingCellSwipe;
|
|
@synthesize swipeDirection = _swipeDirection;
|
|
@synthesize swipeObject = _swipeObject;
|
|
|
|
@synthesize showsOverlayImagesModally = _modalOverlay;
|
|
@synthesize overlayFrame = _overlayFrame;
|
|
@synthesize tableOverlayView = _tableOverlayView;
|
|
@synthesize stateOverlayImageView = _stateOverlayImageView;
|
|
@synthesize cache = _cache;
|
|
|
|
#pragma mark - Instantiation
|
|
|
|
+ (id)tableControllerWithTableView:(UITableView*)tableView
|
|
forViewController:(UIViewController*)viewController {
|
|
return [[[self alloc] initWithTableView:tableView viewController:viewController] autorelease];
|
|
}
|
|
|
|
+ (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;
|
|
self.viewController = theViewController;
|
|
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];
|
|
}
|
|
|
|
_sections = [NSMutableArray new];
|
|
self.objectManager = [RKObjectManager sharedManager];
|
|
_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:@"loading"
|
|
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
|
context:nil];
|
|
[self addObserver:self
|
|
forKeyPath:@"loaded"
|
|
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
|
context:nil];
|
|
[self addObserver:self
|
|
forKeyPath:@"empty"
|
|
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
|
context:nil];
|
|
[self addObserver:self
|
|
forKeyPath:@"error"
|
|
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
|
context:nil];
|
|
[self addObserver:self
|
|
forKeyPath:@"online"
|
|
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;
|
|
_tableView = nil;
|
|
|
|
// Remove overlay and pull-to-refresh subviews
|
|
[_stateOverlayImageView removeFromSuperview];
|
|
[_stateOverlayImageView release];
|
|
_stateOverlayImageView = nil;
|
|
[_tableOverlayView removeFromSuperview];
|
|
[_tableOverlayView release];
|
|
_tableOverlayView = nil;
|
|
|
|
// Remove observers
|
|
[self removeObserver:self forKeyPath:@"loading"];
|
|
[self removeObserver:self forKeyPath:@"loaded"];
|
|
[self removeObserver:self forKeyPath:@"empty"];
|
|
[self removeObserver:self forKeyPath:@"error"];
|
|
[self removeObserver:self forKeyPath:@"online"];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
|
|
// TODO: WTF? Get UI crashes when enabled...
|
|
// [_objectManager.requestQueue abortRequestsWithDelegate:self];
|
|
_objectLoader.delegate = nil;
|
|
_objectLoader = nil;
|
|
|
|
[_sections release];
|
|
[_cellMappings release];
|
|
[_headerItems release];
|
|
[_footerItems release];
|
|
[_cellSwipeView release];
|
|
[_swipeCell release];
|
|
[_swipeObject release];
|
|
[_emptyItem release];
|
|
[super dealloc];
|
|
}
|
|
|
|
- (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 {
|
|
_viewController = 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;
|
|
|
|
// 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) {
|
|
self.online = objectManager.isOnline;
|
|
}
|
|
}
|
|
|
|
- (void)setAutoResizesForKeyboard:(BOOL)autoResizesForKeyboard {
|
|
if (_autoResizesForKeyboard != autoResizesForKeyboard) {
|
|
_autoResizesForKeyboard = autoResizesForKeyboard;
|
|
if (_autoResizesForKeyboard) {
|
|
// Register for Keyboard notifications
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(resizeTableViewForKeyboard:)
|
|
name:UIKeyboardWillShowNotification
|
|
object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(resizeTableViewForKeyboard:)
|
|
name:UIKeyboardWillHideNotification
|
|
object:nil];
|
|
} else {
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (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)objectManagerConnectivityDidChange:(NSNotification *)notification {
|
|
RKLogTrace(@"%@ received network status change notification: %@", self, [notification name]);
|
|
self.online = self.objectManager.isOnline;
|
|
}
|
|
|
|
#pragma mark - Managing Sections
|
|
|
|
- (NSUInteger)sectionCount {
|
|
return [_sections count];
|
|
}
|
|
|
|
- (NSUInteger)rowCount {
|
|
return [[_sections valueForKeyPath:@"@sum.rowCount"] intValue];
|
|
}
|
|
|
|
- (RKTableSection *)sectionAtIndex:(NSUInteger)index {
|
|
return [_sections objectAtIndex:index];
|
|
}
|
|
|
|
- (NSUInteger)indexForSection:(RKTableSection *)section {
|
|
NSAssert(section, @"Cannot return index for a nil section");
|
|
return [_sections indexOfObject:section];
|
|
}
|
|
|
|
- (RKTableSection *)sectionWithHeaderTitle:(NSString *)title {
|
|
for (RKTableSection* section in _sections) {
|
|
if ([section.headerTitle isEqualToString:title]) {
|
|
return section;
|
|
}
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (UITableViewCell *)cellForObjectAtIndexPath:(NSIndexPath *)indexPath {
|
|
RKTableSection* section = [self sectionAtIndex:indexPath.section];
|
|
id mappableObject = [section objectAtIndex:indexPath.row];
|
|
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 = [cellMapping mappableObjectForData:self.tableView];
|
|
NSAssert(cell, @"Cell mapping failed to dequeue or allocate a tableViewCell for object: %@", mappableObject);
|
|
|
|
// Map the object state into the cell
|
|
RKObjectMappingOperation* mappingOperation = [[RKObjectMappingOperation alloc] initWithSourceObject:mappableObject destinationObject:cell mapping:cellMapping];
|
|
NSError* error = nil;
|
|
BOOL success = [mappingOperation performMapping:&error];
|
|
[mappingOperation release];
|
|
// 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 to generate table cell for object: %@", error);
|
|
return nil;
|
|
}
|
|
|
|
return cell;
|
|
}
|
|
|
|
#pragma mark - UITableViewDataSource methods
|
|
|
|
- (NSInteger)numberOfSectionsInTableView:(UITableView*)theTableView {
|
|
NSAssert(theTableView == self.tableView, @"numberOfSectionsInTableView: invoked with inappropriate tableView: %@", theTableView);
|
|
RKLogTrace(@"%@ numberOfSectionsInTableView = %d", self, self.sectionCount);
|
|
return self.sectionCount;
|
|
}
|
|
|
|
- (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);
|
|
return [[_sections objectAtIndex:section] rowCount];
|
|
}
|
|
|
|
- (UITableViewCell *)tableView:(UITableView*)theTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
NSAssert(theTableView == self.tableView, @"tableView:cellForRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView);
|
|
UITableViewCell* cell = [self cellForObjectAtIndexPath:indexPath];
|
|
|
|
RKLogTrace(@"%@ cellForRowAtIndexPath:%@ = %@", self, indexPath, cell);
|
|
return cell;
|
|
}
|
|
|
|
- (NSString*)tableView:(UITableView*)theTableView titleForHeaderInSection:(NSInteger)section {
|
|
NSAssert(theTableView == self.tableView, @"tableView:titleForHeaderInSection: invoked with inappropriate tableView: %@", theTableView);
|
|
return [[_sections objectAtIndex:section] headerTitle];
|
|
}
|
|
|
|
- (NSString*)tableView:(UITableView*)theTableView titleForFooterInSection:(NSInteger)section {
|
|
NSAssert(theTableView == self.tableView, @"tableView:titleForFooterInSection: invoked with inappropriate tableView: %@", theTableView);
|
|
return [[_sections objectAtIndex:section] footerTitle];
|
|
}
|
|
|
|
- (BOOL)tableView:(UITableView*)theTableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
NSAssert(theTableView == self.tableView, @"tableView:canEditRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView);
|
|
return _canEditRows;
|
|
}
|
|
|
|
- (BOOL)tableView:(UITableView*)theTableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
NSAssert(theTableView == self.tableView, @"tableView:canMoveRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView);
|
|
return _canMoveRows;
|
|
}
|
|
|
|
#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];
|
|
}
|
|
|
|
- (id)objectForRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
NSAssert(indexPath, @"Cannot lookup object with a nil indexPath");
|
|
RKTableSection* section = [self sectionAtIndex:indexPath.section];
|
|
return [section objectAtIndex:indexPath.row];
|
|
}
|
|
|
|
- (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 cellForObjectAtIndexPath:indexPath] : nil;
|
|
}
|
|
|
|
- (NSIndexPath *)indexPathForObject:(id)object {
|
|
NSUInteger sectionIndex = 0;
|
|
for (RKTableSection *section in self.sections) {
|
|
NSUInteger rowIndex = 0;
|
|
for (id rowObject in section.objects) {
|
|
if ([rowObject isEqual:object]) {
|
|
return [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex];
|
|
}
|
|
|
|
rowIndex++;
|
|
}
|
|
sectionIndex++;
|
|
}
|
|
|
|
return 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 - 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];
|
|
|
|
// NOTE: Do NOT use cellForObjectAtIndexPath here. See https://gist.github.com/eafbb641d37bb7137759
|
|
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);
|
|
}
|
|
}
|
|
|
|
- (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);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
- (CGFloat)tableView:(UITableView*)theTableView heightForHeaderInSection:(NSInteger)sectionIndex {
|
|
NSAssert(theTableView == self.tableView, @"heightForHeaderInSection: invoked with inappropriate tableView: %@", theTableView);
|
|
RKTableSection* section = [self sectionAtIndex:sectionIndex];
|
|
return section.headerHeight;
|
|
}
|
|
|
|
- (CGFloat)tableView:(UITableView*)theTableView heightForFooterInSection:(NSInteger)sectionIndex {
|
|
NSAssert(theTableView == self.tableView, @"heightForFooterInSection: invoked with inappropriate tableView: %@", theTableView);
|
|
RKTableSection* section = [self sectionAtIndex:sectionIndex];
|
|
return section.footerHeight;
|
|
}
|
|
|
|
- (UIView*)tableView:(UITableView*)theTableView viewForHeaderInSection:(NSInteger)sectionIndex {
|
|
NSAssert(theTableView == self.tableView, @"viewForHeaderInSection: invoked with inappropriate tableView: %@", theTableView);
|
|
RKTableSection* section = [self sectionAtIndex:sectionIndex];
|
|
return section.headerView;
|
|
}
|
|
|
|
- (UIView*)tableView:(UITableView*)theTableView viewForFooterInSection:(NSInteger)sectionIndex {
|
|
NSAssert(theTableView == self.tableView, @"viewForFooterInSection: invoked with inappropriate tableView: %@", theTableView);
|
|
RKTableSection* section = [self sectionAtIndex:sectionIndex];
|
|
return section.footerView;
|
|
}
|
|
|
|
- (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.objectLoader cancel];
|
|
}
|
|
|
|
- (NSDate*)lastUpdatedDate {
|
|
if (! self.objectLoader) {
|
|
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.objectLoader.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.loading = YES;
|
|
}
|
|
|
|
- (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.error = error;
|
|
[self didFinishLoad];
|
|
}
|
|
|
|
- (void)objectLoaderDidFinishLoading:(RKObjectLoader *)objectLoader {
|
|
if ([self.delegate respondsToSelector:@selector(tableController:didLoadTableWithObjectLoader:)]) {
|
|
[self.delegate tableController:self didLoadTableWithObjectLoader:objectLoader];
|
|
}
|
|
|
|
[self.objectLoader reset];
|
|
[self didFinishLoad];
|
|
}
|
|
|
|
- (void)didFinishLoad {
|
|
self.empty = [self isEmpty];
|
|
self.loading = [self.objectLoader isLoading]; // Mutate loading state after we have adjusted empty
|
|
self.loaded = YES;
|
|
|
|
// Setup offline image state based on current online/offline state
|
|
[self updateOfflineImageForOnlineState:[self isOnline]];
|
|
|
|
[self resetOverlayView];
|
|
|
|
if (self.delegate && [_delegate respondsToSelector:@selector(tableControllerDidFinishFinalLoad:)])
|
|
[_delegate performSelector:@selector(tableControllerDidFinishFinalLoad:)];
|
|
}
|
|
|
|
#pragma mark - Table Overlay Views
|
|
|
|
// 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 release];
|
|
_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 {
|
|
[imageForEmpty retain];
|
|
BOOL imageRemoved = [self removeImageFromOverlay:_imageForEmpty];
|
|
[_imageForEmpty release];
|
|
_imageForEmpty = imageForEmpty;
|
|
if (imageRemoved) [self showImageInOverlay:_imageForEmpty];
|
|
}
|
|
|
|
- (void)setImageForError:(UIImage*)imageForError {
|
|
[imageForError retain];
|
|
BOOL imageRemoved = [self removeImageFromOverlay:_imageForError];
|
|
[_imageForError release];
|
|
_imageForError = imageForError;
|
|
if (imageRemoved) [self showImageInOverlay:_imageForError];
|
|
}
|
|
|
|
- (void)setImageForOffline:(UIImage*)imageForOffline {
|
|
[imageForOffline retain];
|
|
BOOL imageRemoved = [self removeImageFromOverlay:_imageForOffline];
|
|
[_imageForOffline release];
|
|
_imageForOffline = imageForOffline;
|
|
if (imageRemoved) [self showImageInOverlay:_imageForOffline];
|
|
}
|
|
|
|
- (void)setLoadingView:(UIView*)loadingView {
|
|
[loadingView retain];
|
|
BOOL viewRemoved = (_loadingView.superview != nil);
|
|
[_loadingView removeFromSuperview];
|
|
[self resetOverlayView];
|
|
[_loadingView release];
|
|
_loadingView = loadingView;
|
|
if (viewRemoved) [self addToOverlayView:_loadingView modally:NO];
|
|
}
|
|
|
|
#pragma mark - KVO & Model States
|
|
|
|
- (BOOL)isLoading {
|
|
return self.loading;
|
|
}
|
|
|
|
- (BOOL)isLoaded {
|
|
return self.loaded;
|
|
}
|
|
|
|
- (BOOL)isOnline {
|
|
return self.online;
|
|
}
|
|
|
|
- (BOOL)isError {
|
|
return _error != nil;
|
|
}
|
|
|
|
- (BOOL)isEmpty {
|
|
NSUInteger nonRowItemsCount = [_headerItems count] + [_footerItems count];
|
|
nonRowItemsCount += _emptyItem ? 1 : 0;
|
|
BOOL isEmpty = (self.rowCount - nonRowItemsCount) == 0;
|
|
RKLogTrace(@"Determined isEmpty = %@. self.rowCount = %d with %d nonRowItems in the table", isEmpty ? @"YES" : @"NO", self.rowCount, nonRowItemsCount);
|
|
return isEmpty;
|
|
}
|
|
|
|
- (void)isLoadingDidChangeTo:(BOOL)isLoading {
|
|
if (isLoading) {
|
|
// Remove any current state to allow drawing of the loading view
|
|
[self removeImageOverlay];
|
|
|
|
// Clear the error state
|
|
self.error = nil;
|
|
self.empty = NO;
|
|
|
|
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 = isLoading;
|
|
}
|
|
|
|
- (void)isLoadedDidChangeTo:(BOOL)isLoaded {
|
|
if (isLoaded) {
|
|
RKLogDebug(@"%@: is now loaded.", self);
|
|
} else {
|
|
RKLogDebug(@"%@: is NOT loaded.", self);
|
|
}
|
|
}
|
|
|
|
- (void)errorDidChangeTo:(BOOL)isError {
|
|
if (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];
|
|
|
|
if (self.imageForError) {
|
|
[self showImageInOverlay:self.imageForError];
|
|
}
|
|
} else {
|
|
[self removeImageFromOverlay:self.imageForError];
|
|
}
|
|
}
|
|
|
|
- (void)isEmptyDidChangeTo:(BOOL)isEmpty {
|
|
if (isEmpty) {
|
|
// TODO: maybe this should be didLoadEmpty?
|
|
if ([self.delegate respondsToSelector:@selector(tableControllerDidBecomeEmpty:)]) {
|
|
[self.delegate tableControllerDidBecomeEmpty:self];
|
|
}
|
|
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadEmptyNotification object:self];
|
|
|
|
if (self.imageForEmpty) {
|
|
[self showImageInOverlay:self.imageForEmpty];
|
|
}
|
|
} else {
|
|
if (self.imageForEmpty) {
|
|
[self removeImageFromOverlay:self.imageForEmpty];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)updateOfflineImageForOnlineState:(BOOL)isOnline {
|
|
if (isOnline) {
|
|
[self removeImageFromOverlay:self.imageForOffline];
|
|
} else {
|
|
if (self.imageForOffline) {
|
|
[self showImageInOverlay:self.imageForOffline];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)isOnlineDidChangeTo:(BOOL)isOnline {
|
|
if (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];
|
|
}
|
|
|
|
[self updateOfflineImageForOnlineState:isOnline];
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
|
|
BOOL newValue = NO;
|
|
BOOL oldValue = NO;
|
|
if ([keyPath isEqualToString:@"loading"]) {
|
|
newValue = [[change valueForKey:NSKeyValueChangeNewKey] boolValue];
|
|
oldValue = [[change valueForKey:NSKeyValueChangeOldKey] boolValue];
|
|
if (newValue != oldValue) [self isLoadingDidChangeTo:newValue];
|
|
} else if ([keyPath isEqualToString:@"loaded"]) {
|
|
newValue = [[change valueForKey:NSKeyValueChangeNewKey] boolValue];
|
|
oldValue = [[change valueForKey:NSKeyValueChangeOldKey] boolValue];
|
|
if (newValue != oldValue) [self isLoadedDidChangeTo:newValue];
|
|
} else if ([keyPath isEqualToString:@"error"]) {
|
|
newValue = (! [[change valueForKey:NSKeyValueChangeNewKey] isEqual:[NSNull null]]);
|
|
oldValue = (! [[change valueForKey:NSKeyValueChangeOldKey] isEqual:[NSNull null]]);
|
|
if (newValue != oldValue) [self errorDidChangeTo:newValue];
|
|
} else if ([keyPath isEqualToString:@"empty"]) {
|
|
newValue = [[change valueForKey:NSKeyValueChangeNewKey] boolValue];
|
|
oldValue = [[change valueForKey:NSKeyValueChangeOldKey] boolValue];
|
|
if (newValue != oldValue) [self isEmptyDidChangeTo:newValue];
|
|
} else if ([keyPath isEqualToString:@"online"]) {
|
|
newValue = [[change valueForKey:NSKeyValueChangeNewKey] boolValue];
|
|
oldValue = [[change valueForKey:NSKeyValueChangeOldKey] boolValue];
|
|
if (newValue != oldValue) [self isOnlineDidChangeTo:newValue];
|
|
}
|
|
|
|
RKLogTrace(@"Key-value observation triggered for keyPath '%@'. Old value = %d, new value = %d", keyPath, oldValue, newValue);
|
|
}
|
|
|
|
#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:)] autorelease];
|
|
[self.tableView addGestureRecognizer:recognizer];
|
|
}
|
|
else {
|
|
recognizer = [self pullToRefreshGestureRecognizer];
|
|
if (recognizer)
|
|
[self.tableView removeGestureRecognizer:recognizer];
|
|
}
|
|
_pullToRefreshEnabled = pullToRefreshEnabled;
|
|
}
|
|
|
|
- (void)pullToRefreshStateChanged:(UIGestureRecognizer *)gesture {
|
|
if (gesture.state == UIGestureRecognizerStateRecognized) {
|
|
if ([self pullToRefreshDataSourceIsLoading:gesture])
|
|
return;
|
|
RKLogDebug(@"%@: pull to refresh triggered from gesture: %@", self, gesture);
|
|
if (self.objectLoader) {
|
|
[self.objectLoader reset];
|
|
[self.objectLoader send];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (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];
|
|
[rightSwipeGestureRecognizer release];
|
|
|
|
// Setup a left swipe gesture recognizer
|
|
UISwipeGestureRecognizer* leftSwipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeft:)];
|
|
leftSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionLeft;
|
|
[self.tableView addGestureRecognizer:leftSwipeGestureRecognizer];
|
|
[leftSwipeGestureRecognizer release];
|
|
}
|
|
|
|
- (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 retain];
|
|
_swipeObject = [object retain];
|
|
_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 release];
|
|
_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 release];
|
|
_swipeCell = nil;
|
|
[_cellSwipeView removeFromSuperview];
|
|
}
|
|
|
|
#pragma mark UIScrollViewDelegate methods
|
|
|
|
- (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView {
|
|
[self removeSwipeView:YES];
|
|
}
|
|
|
|
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView {
|
|
[self removeSwipeView:NO];
|
|
return YES;
|
|
}
|
|
|
|
#pragma mark - Keyboard Notification methods
|
|
|
|
- (void)resizeTableViewForKeyboard:(NSNotification*)notification {
|
|
NSAssert(_autoResizesForKeyboard, @"Errantly receiving keyboard notifications while autoResizesForKeyboard=NO");
|
|
NSDictionary* userInfo = [notification userInfo];
|
|
|
|
CGRect keyboardEndFrame = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
|
CGFloat heightForViewShift = keyboardEndFrame.size.height;
|
|
RKLogTrace(@"keyboardEndFrame.size.height=%f, heightForViewShift=%f",
|
|
keyboardEndFrame.size.height, heightForViewShift);
|
|
|
|
CGFloat bottomBarOffset = 0.0;
|
|
UINavigationController* navigationController = self.viewController.navigationController;
|
|
if (navigationController && navigationController.toolbar && !navigationController.toolbarHidden) {
|
|
bottomBarOffset += navigationController.toolbar.frame.size.height;
|
|
RKLogTrace(@"Found a visible toolbar. Reducing size of heightForViewShift by=%f", bottomBarOffset);
|
|
}
|
|
|
|
UITabBarController* tabBarController = self.viewController.tabBarController;
|
|
if (tabBarController && tabBarController.tabBar && !self.viewController.hidesBottomBarWhenPushed) {
|
|
bottomBarOffset += tabBarController.tabBar.frame.size.height;
|
|
RKLogTrace(@"Found a visible tabBar. Reducing size of heightForViewShift by=%f", bottomBarOffset);
|
|
}
|
|
|
|
if ([[notification name] isEqualToString:UIKeyboardWillShowNotification]) {
|
|
[UIView beginAnimations:nil context:nil];
|
|
[UIView setAnimationDuration:0.2];
|
|
UIEdgeInsets contentInsets = UIEdgeInsetsMake(0, 0, (heightForViewShift - bottomBarOffset), 0);
|
|
self.tableView.contentInset = contentInsets;
|
|
self.tableView.scrollIndicatorInsets = contentInsets;
|
|
|
|
CGRect nonKeyboardRect = self.tableView.frame;
|
|
nonKeyboardRect.size.height -= heightForViewShift;
|
|
RKLogTrace(@"Searching for a firstResponder not inside our nonKeyboardRect (%f, %f, %f, %f)",
|
|
nonKeyboardRect.origin.x, nonKeyboardRect.origin.y,
|
|
nonKeyboardRect.size.width, nonKeyboardRect.size.height);
|
|
|
|
UIView* firstResponder = [self.tableView findFirstResponder];
|
|
if (firstResponder) {
|
|
CGRect firstResponderFrame = firstResponder.frame;
|
|
RKLogTrace(@"Found firstResponder=%@ at (%f, %f, %f, %f)", firstResponder,
|
|
firstResponderFrame.origin.x, firstResponderFrame.origin.y,
|
|
firstResponderFrame.size.width, firstResponderFrame.size.width);
|
|
|
|
if (![firstResponder.superview isEqual:self.tableView]) {
|
|
firstResponderFrame = [firstResponder.superview convertRect:firstResponderFrame toView:self.tableView];
|
|
RKLogTrace(@"firstResponder (%@) frame is not in tableView's coordinate system. Coverted to (%f, %f, %f, %f)",
|
|
firstResponder, firstResponderFrame.origin.x, firstResponderFrame.origin.y,
|
|
firstResponderFrame.size.width, firstResponderFrame.size.height);
|
|
}
|
|
|
|
if (!CGRectContainsPoint(nonKeyboardRect, firstResponderFrame.origin)) {
|
|
RKLogTrace(@"firstResponder (%@) is underneath keyboard. Beginning scroll of tableView to show", firstResponder);
|
|
[self.tableView scrollRectToVisible:firstResponderFrame animated:YES];
|
|
}
|
|
}
|
|
[UIView commitAnimations];
|
|
|
|
} else if ([[notification name] isEqualToString:UIKeyboardWillHideNotification]) {
|
|
[UIView beginAnimations:nil context:nil];
|
|
[UIView setAnimationDuration:0.2];
|
|
UIEdgeInsets contentInsets = UIEdgeInsetsZero;
|
|
self.tableView.contentInset = contentInsets;
|
|
self.tableView.scrollIndicatorInsets = contentInsets;
|
|
[UIView commitAnimations];
|
|
}
|
|
}
|
|
|
|
- (void)loadTableWithObjectLoader:(RKObjectLoader*)theObjectLoader {
|
|
NSAssert(theObjectLoader, @"Cannot perform a network load without an object loader");
|
|
if (! [self.objectLoader isEqual:theObjectLoader]) {
|
|
theObjectLoader.delegate = self;
|
|
self.objectLoader = theObjectLoader;
|
|
}
|
|
if ([self.delegate respondsToSelector:@selector(tableController:willLoadTableWithObjectLoader:)]) {
|
|
[self.delegate tableController:self willLoadTableWithObjectLoader:self.objectLoader];
|
|
}
|
|
if (self.objectLoader.queue && ![self.objectLoader.queue containsRequest:self.objectLoader]) {
|
|
[self.objectLoader.queue addRequest:self.objectLoader];
|
|
}
|
|
}
|
|
|
|
@end
|