// // 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; 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) { 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; } - (NSUInteger)numberOfRowsInSectionAtIndex:(NSUInteger)index { return [self sectionAtIndex:index].rowCount; } - (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