From dcef1b1c163a05fd0f5bea9109dbb92b69113af9 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sat, 19 May 2012 17:04:59 -0400 Subject: [PATCH 01/12] Bump version to 0.10.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 539f9fc6..57121573 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.10.1-dev +0.10.1 From 5cecb58c2387f70b55474043e95c2a95b253fbb5 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Mon, 21 May 2012 12:58:44 -0400 Subject: [PATCH 02/12] Add RKLog helper for displaying detailed key-value validation failure information. closes #750 --- Code/ObjectMapping/RKObjectMappingOperation.m | 3 +- Code/Support/RKLog.h | 6 ++++ Code/Support/RKLog.m | 32 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Code/ObjectMapping/RKObjectMappingOperation.m b/Code/ObjectMapping/RKObjectMappingOperation.m index 04ddd545..711150e1 100644 --- a/Code/ObjectMapping/RKObjectMappingOperation.m +++ b/Code/ObjectMapping/RKObjectMappingOperation.m @@ -252,7 +252,8 @@ BOOL RKObjectIsValueEqualToValue(id sourceValue, id destinationValue) { success = [self.destinationObject validateValue:value forKeyPath:keyPath error:&_validationError]; if (!success) { if (_validationError) { - RKLogError(@"Validation failed while mapping attribute at key path %@ to value %@. Error: %@", keyPath, *value, [_validationError localizedDescription]); + RKLogError(@"Validation failed while mapping attribute at key path '%@' to value %@. Error: %@", keyPath, *value, [_validationError localizedDescription]); + RKLogValidationError(_validationError); } else { RKLogWarning(@"Destination object %@ rejected attribute value %@ for keyPath %@. Skipping...", self.destinationObject, *value, keyPath); } diff --git a/Code/Support/RKLog.h b/Code/Support/RKLog.h index 0bf7ba90..794a9aa9 100644 --- a/Code/Support/RKLog.h +++ b/Code/Support/RKLog.h @@ -199,3 +199,9 @@ void RKLogInitialize(void); Trace or 6 */ void RKLogConfigureFromEnvironment(void); + +/** + Logs extensive information about an NSError generated as the results + of a failed key-value validation error. + */ +void RKLogValidationError(NSError *); diff --git a/Code/Support/RKLog.m b/Code/Support/RKLog.m index 4b67c297..71dc34d2 100644 --- a/Code/Support/RKLog.m +++ b/Code/Support/RKLog.m @@ -127,3 +127,35 @@ int RKLogLevelForString(NSString *logLevel, NSString *envVarName) return -1; } } + +void RKLogValidationError(NSError *validationError) { + if ([[validationError domain] isEqualToString:@"NSCocoaErrorDomain"]) { + NSDictionary *userInfo = [validationError userInfo]; + NSArray *errors = [userInfo valueForKey:@"NSDetailedErrors"]; + if (errors) { + for (NSError *detailedError in errors) { + NSDictionary *subUserInfo = [detailedError userInfo]; + RKLogError(@"Core Data Save Error\n \ + NSLocalizedDescription:\t\t%@\n \ + NSValidationErrorKey:\t\t\t%@\n \ + NSValidationErrorPredicate:\t%@\n \ + NSValidationErrorObject:\n%@\n", + [subUserInfo valueForKey:@"NSLocalizedDescription"], + [subUserInfo valueForKey:@"NSValidationErrorKey"], + [subUserInfo valueForKey:@"NSValidationErrorPredicate"], + [subUserInfo valueForKey:@"NSValidationErrorObject"]); + } + } + else { + RKLogError(@"Core Data Save Error\n \ + NSLocalizedDescription:\t\t%@\n \ + NSValidationErrorKey:\t\t\t%@\n \ + NSValidationErrorPredicate:\t%@\n \ + NSValidationErrorObject:\n%@\n", + [userInfo valueForKey:@"NSLocalizedDescription"], + [userInfo valueForKey:@"NSValidationErrorKey"], + [userInfo valueForKey:@"NSValidationErrorPredicate"], + [userInfo valueForKey:@"NSValidationErrorObject"]); + } + } +} From 871ab1389effd40dd018a61efaeec28a5ff881ee Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Mon, 21 May 2012 13:00:29 -0400 Subject: [PATCH 03/12] Ensure managed object connections are established across appropriate managed object contexts --- Code/CoreData/RKManagedObjectMappingOperation.m | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Code/CoreData/RKManagedObjectMappingOperation.m b/Code/CoreData/RKManagedObjectMappingOperation.m index 8f26f9e9..4d52293e 100644 --- a/Code/CoreData/RKManagedObjectMappingOperation.m +++ b/Code/CoreData/RKManagedObjectMappingOperation.m @@ -84,13 +84,17 @@ // Normal foreign key NSObject *cache = [[(RKManagedObjectMapping*)[self objectMapping] objectStore] cacheStrategy]; - relatedObject = [cache findInstanceOfEntity:objectMapping.entity withPrimaryKeyAttribute:primaryKeyAttributeOfRelatedObject value:valueOfLocalPrimaryKeyAttribute inManagedObjectContext:[[(RKManagedObjectMapping*)[self objectMapping] objectStore] managedObjectContextForCurrentThread]]; + relatedObject = [cache findInstanceOfEntity:objectMapping.entity withPrimaryKeyAttribute:primaryKeyAttributeOfRelatedObject value:valueOfLocalPrimaryKeyAttribute inManagedObjectContext:[self.destinationObject managedObjectContext]]; } if (relatedObject) { RKLogDebug(@"Connected relationship '%@' to object with primary key value '%@': %@", relationshipName, valueOfLocalPrimaryKeyAttribute, relatedObject); } else { RKLogDebug(@"Failed to find instance of '%@' to connect relationship '%@' with primary key value '%@'", [[objectMapping entity] name], relationshipName, valueOfLocalPrimaryKeyAttribute); } + if ([relatedObject isKindOfClass:[NSManagedObject class]]) { + // Sanity check the managed object contexts + NSAssert([[(NSManagedObject *)self.destinationObject managedObjectContext] isEqual:[(NSManagedObject *)relatedObject managedObjectContext]], nil); + } RKLogTrace(@"setValue of %@ forKeyPath %@", relatedObject, relationshipName); [self.destinationObject setValue:relatedObject forKeyPath:relationshipName]; } else { @@ -99,7 +103,7 @@ } - (void)connectRelationships { - NSDictionary* relationshipsAndPrimaryKeyAttributes = [(RKManagedObjectMapping*)self.objectMapping relationshipsAndPrimaryKeyAttributes]; + NSDictionary* relationshipsAndPrimaryKeyAttributes = [(RKManagedObjectMapping *)self.objectMapping relationshipsAndPrimaryKeyAttributes]; RKLogTrace(@"relationshipsAndPrimaryKeyAttributes: %@", relationshipsAndPrimaryKeyAttributes); for (NSString* relationshipName in relationshipsAndPrimaryKeyAttributes) { if (self.queue) { @@ -124,7 +128,6 @@ manually invoke processPendingChanges to prevent recreating objects with the same primary key. See https://github.com/RestKit/RestKit/issues/661 */ -// [[[(RKManagedObjectMapping *)self.objectMapping objectStore] managedObjectContextForCurrentThread] processPendingChanges]; [self connectRelationships]; } return success; From 9ca2ba2a3fe601e82745f5695cb5b9d000e11572 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Mon, 21 May 2012 18:09:08 -0400 Subject: [PATCH 04/12] Clean up inconsistencies with 'cancelled' and 'cancel' to match Cocoa (isCancelled) --- Code/Network/RKRequest.m | 13 ++++++------- Code/Network/RKRequestQueue.h | 4 ++-- Code/Network/RKRequestQueue.m | 4 ++-- Code/ObjectMapping/RKObjectLoader.m | 7 +++++-- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Code/Network/RKRequest.m b/Code/Network/RKRequest.m index 455d9604..3cd10896 100644 --- a/Code/Network/RKRequest.m +++ b/Code/Network/RKRequest.m @@ -88,7 +88,7 @@ RKRequestMethod RKRequestMethodTypeFromName(NSString *methodName) { @interface RKRequest () @property (nonatomic, assign, readwrite, getter = isLoaded) BOOL loaded; @property (nonatomic, assign, readwrite, getter = isLoading) BOOL loading; -@property (nonatomic, assign, readwrite) BOOL canceled; +@property (nonatomic, assign, readwrite, getter = isCancelled) BOOL cancelled; @property (nonatomic, retain, readwrite) RKResponse *response; @end @@ -123,13 +123,12 @@ RKRequestMethod RKRequestMethodTypeFromName(NSString *methodName) { @synthesize onDidFailLoadWithError; @synthesize additionalRootCertificates = _additionalRootCertificates; @synthesize disableCertificateValidation = _disableCertificateValidation; -@synthesize cancelled = _cancelled; @synthesize followRedirect = _followRedirect; @synthesize runLoopMode = _runLoopMode; @synthesize loaded = _loaded; @synthesize loading = _loading; -@synthesize canceled = _canceled; @synthesize response = _response; +@synthesize cancelled = _cancelled; #if TARGET_OS_IPHONE @synthesize backgroundPolicy = _backgroundPolicy; @@ -184,7 +183,7 @@ RKRequestMethod RKRequestMethodTypeFromName(NSString *methodName) { _connection = nil; self.loading = NO; self.loaded = NO; - self.canceled = NO; + self.cancelled = NO; } - (void)cleanupBackgroundTask { @@ -407,7 +406,7 @@ RKRequestMethod RKRequestMethodTypeFromName(NSString *methodName) { } - (void)cancelAndInformDelegate:(BOOL)informDelegate { - _cancelled = YES; + self.cancelled = YES; [_connection cancel]; [_connection release]; _connection = nil; @@ -436,7 +435,7 @@ RKRequestMethod RKRequestMethodTypeFromName(NSString *methodName) { - (void)fireAsynchronousRequest { RKLogDebug(@"Sending asynchronous %@ request to URL %@.", [self HTTPMethod], [[self URL] absoluteString]); if (![self prepareURLRequest]) { - // TODO: Logging + RKLogWarning(@"Failed to send request asynchronously: prepareURLRequest returned NO."); return; } @@ -565,7 +564,7 @@ RKRequestMethod RKRequestMethodTypeFromName(NSString *methodName) { RKLogDebug(@"Sending synchronous %@ request to URL %@.", [self HTTPMethod], [[self URL] absoluteString]); if (![self prepareURLRequest]) { - // TODO: Logging + RKLogWarning(@"Failed to send request synchronously: prepareURLRequest returned NO."); return nil; } diff --git a/Code/Network/RKRequestQueue.h b/Code/Network/RKRequestQueue.h index bb57c557..f7308f06 100644 --- a/Code/Network/RKRequestQueue.h +++ b/Code/Network/RKRequestQueue.h @@ -159,7 +159,7 @@ reference and canceling the request. Useful when an object that acts as the delegate for one or more requests - is being deallocated and all outstanding requests should be canceled + is being deallocated and all outstanding requests should be cancelled without generating any further delegate callbacks. @param delegate The object acting as the delegate for all enqueued requests that are to be aborted. @@ -314,7 +314,7 @@ - (void)requestQueue:(RKRequestQueue *)queue didLoadResponse:(RKResponse *)response; /** - Sent when queue has canceled a request. + Sent when queue has cancelled a request. @param queue The queue that cancelled the request. @param request The cancelled request. diff --git a/Code/Network/RKRequestQueue.m b/Code/Network/RKRequestQueue.m index fee75e9f..88eb6373 100644 --- a/Code/Network/RKRequestQueue.m +++ b/Code/Network/RKRequestQueue.m @@ -389,7 +389,7 @@ static const NSTimeInterval kFlushDelay = 0.3; - (void)cancelRequest:(RKRequest*)request loadNext:(BOOL)loadNext { if ([request isUnsent]) { - RKLogDebug(@"Canceled undispatched request %@ and removed from queue %@", request, self); + RKLogDebug(@"Cancelled undispatched request %@ and removed from queue %@", request, self); [self removeRequest:request]; request.delegate = nil; @@ -398,7 +398,7 @@ static const NSTimeInterval kFlushDelay = 0.3; [_delegate requestQueue:self didCancelRequest:request]; } } else if ([self containsRequest:request] && [request isLoading]) { - RKLogDebug(@"Canceled loading request %@ and removed from queue %@", request, self); + RKLogDebug(@"Cancelled loading request %@ and removed from queue %@", request, self); [request cancel]; request.delegate = nil; diff --git a/Code/ObjectMapping/RKObjectLoader.m b/Code/ObjectMapping/RKObjectLoader.m index 0d236257..eed31076 100644 --- a/Code/ObjectMapping/RKObjectLoader.m +++ b/Code/ObjectMapping/RKObjectLoader.m @@ -123,7 +123,7 @@ - (void)finalizeLoad:(BOOL)successful { self.loading = NO; self.loaded = successful; - + if ([self.delegate respondsToSelector:@selector(objectLoaderDidFinishLoading:)]) { [(NSObject*)self.delegate performSelectorOnMainThread:@selector(objectLoaderDidFinishLoading:) withObject:self waitUntilDone:YES]; @@ -407,7 +407,10 @@ object:self userInfo:userInfo]; } - [self informDelegateOfError:error]; + + if (! self.isCancelled) { + [self informDelegateOfError:error]; + } [self finalizeLoad:NO]; } From 86ac03895707ba57ec6ec0a4b9909156aa750004 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Mon, 21 May 2012 18:10:13 -0400 Subject: [PATCH 05/12] Cancel current object loader when loading a table view --- Code/UI/RKAbstractTableController.m | 8 +++++++- .../Application/UI/RKFetchedResultsTableControllerTest.m | 2 -- Tests/Application/UI/RKTableControllerTest.m | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Code/UI/RKAbstractTableController.m b/Code/UI/RKAbstractTableController.m index d5d67357..5d9e91b4 100755 --- a/Code/UI/RKAbstractTableController.m +++ b/Code/UI/RKAbstractTableController.m @@ -197,6 +197,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; // TODO: WTF? Get UI crashes when enabled... // [_objectManager.requestQueue abortRequestsWithDelegate:self]; _objectLoader.delegate = nil; + [_objectLoader release]; _objectLoader = nil; [_sections release]; @@ -741,7 +742,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; [self.delegate tableController:self didLoadTableWithObjectLoader:objectLoader]; } - [self.objectLoader reset]; + [objectLoader reset]; [self didFinishLoad]; } @@ -1347,6 +1348,11 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; - (void)loadTableWithObjectLoader:(RKObjectLoader*)theObjectLoader { NSAssert(theObjectLoader, @"Cannot perform a network load without an object loader"); if (! [self.objectLoader isEqual:theObjectLoader]) { + if (self.objectLoader) { + RKLogDebug(@"Cancelling in progress table load: asked to load with a new object loader."); + [self.objectLoader.queue cancelRequest:self.objectLoader]; + } + theObjectLoader.delegate = self; self.objectLoader = theObjectLoader; } diff --git a/Tests/Application/UI/RKFetchedResultsTableControllerTest.m b/Tests/Application/UI/RKFetchedResultsTableControllerTest.m index 3a035d2f..aca2c59a 100644 --- a/Tests/Application/UI/RKFetchedResultsTableControllerTest.m +++ b/Tests/Application/UI/RKFetchedResultsTableControllerTest.m @@ -541,13 +541,11 @@ canEditRowAtIndexPath:deleteIndexPath]; assertThatBool(delegateCanEdit, is(equalToBool(YES))); -// RKTestNotificationObserver* observer = [RKTestNotificationObserver notificationObserverForNotificationName:RKRequestDidLoadResponseNotification]; [RKTestNotificationObserver waitForNotificationWithName:RKRequestDidLoadResponseNotification usingBlock:^{ [tableController tableView:tableController.tableView commitEditingStyle:UITableViewCellEditingStyleDelete forRowAtIndexPath:deleteIndexPath]; }]; -// observer.timeout = 30; assertThatInt([tableController rowCount], is(equalToInt(1))); assertThat([tableController objectForRowAtIndexPath:deleteIndexPath], is(equalTo(other))); diff --git a/Tests/Application/UI/RKTableControllerTest.m b/Tests/Application/UI/RKTableControllerTest.m index 7c57f526..72677057 100644 --- a/Tests/Application/UI/RKTableControllerTest.m +++ b/Tests/Application/UI/RKTableControllerTest.m @@ -764,7 +764,7 @@ RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThatBool([tableController isLoaded], is(equalToBool(NO))); assertThatBool([tableController isLoading], is(equalToBool(NO))); - id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + id mockLoader = [OCMockObject niceMockForClass:[RKObjectLoader class]]; [tableController requestDidStartLoad:mockLoader]; assertThatBool([tableController isLoading], is(equalToBool(YES))); [tableController objectLoaderDidFinishLoading:mockLoader]; From 98c8780a3146dd98927af491e57179a4357dbc9b Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Wed, 23 May 2012 16:27:51 -0400 Subject: [PATCH 06/12] Implemented support for type coercions in primaryKeyAttribute API's. closes #758 --- .../NSEntityDescription+RKAdditions.h | 19 +++- .../NSEntityDescription+RKAdditions.m | 50 ++++++++-- Code/CoreData/NSManagedObject+ActiveRecord.m | 11 +-- .../RKFetchRequestManagedObjectCache.m | 2 +- Code/CoreData/RKManagedObjectMapping.m | 6 +- .../NSEntityDescription+RKAdditionsTest.m | 93 +++++++++++++++++-- .../NSManagedObject+ActiveRecordTest.m | 18 +++- .../CoreData/RKManagedObjectMappingTest.m | 9 +- 8 files changed, 170 insertions(+), 38 deletions(-) diff --git a/Code/CoreData/NSEntityDescription+RKAdditions.h b/Code/CoreData/NSEntityDescription+RKAdditions.h index 4ad13d84..79cb8a16 100644 --- a/Code/CoreData/NSEntityDescription+RKAdditions.h +++ b/Code/CoreData/NSEntityDescription+RKAdditions.h @@ -42,7 +42,17 @@ extern NSString * const RKEntityDescriptionPrimaryKeyAttributeValuePredicateSubs Programmatically configured values take precedence over the user info dictionary. */ -@property (nonatomic, retain) NSString *primaryKeyAttribute; +@property (nonatomic, retain) NSString *primaryKeyAttributeName; + +/** + The attribute description object for the attribute designated as the primary key for the receiver. + */ +@property (nonatomic, readonly) NSAttributeDescription *primaryKeyAttribute; + +/** + The class representing the value of the attribute designated as the primary key for the receiver. + */ +@property (nonatomic, readonly) Class primaryKeyAttributeClass; /** Returns a cached predicate specifying that the primary key attribute is equal to the $PRIMARY_KEY_VALUE @@ -61,7 +71,12 @@ extern NSString * const RKEntityDescriptionPrimaryKeyAttributeValuePredicateSubs value. This predicate is constructed by evaluating the cached predicate returned by the predicateForPrimaryKeyAttribute with a dictionary of substitution variables specifying that $PRIMARY_KEY_VALUE is equal to the given value. - + + **NOTE**: This method considers the type of the receiver's primary key attribute when constructing + the predicate. It will coerce the given value into either an NSString or an NSNumber as + appropriate. This behavior is a convenience to avoid annoying issues related to Core Data's + handling of predicates for NSString and NSNumber types that were not appropriately casted. + @return A predicate speciying that the value of the primary key attribute is equal to a given value. */ - (NSPredicate *)predicateForPrimaryKeyAttributeWithValue:(id)value; diff --git a/Code/CoreData/NSEntityDescription+RKAdditions.m b/Code/CoreData/NSEntityDescription+RKAdditions.m index 5cc8257b..225bc4aa 100644 --- a/Code/CoreData/NSEntityDescription+RKAdditions.m +++ b/Code/CoreData/NSEntityDescription+RKAdditions.m @@ -12,13 +12,13 @@ NSString * const RKEntityDescriptionPrimaryKeyAttributeUserInfoKey = @"primaryKeyAttribute"; NSString * const RKEntityDescriptionPrimaryKeyAttributeValuePredicateSubstitutionVariable = @"PRIMARY_KEY_VALUE"; -static char primaryKeyAttributeKey, primaryKeyPredicateKey; +static char primaryKeyAttributeNameKey, primaryKeyPredicateKey; @implementation NSEntityDescription (RKAdditions) - (void)setPredicateForPrimaryKeyAttribute:(NSString *)primaryKeyAttribute { - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K == $PRIMARY_KEY_VALUE", primaryKeyAttribute]; + NSPredicate *predicate = (primaryKeyAttribute) ? [NSPredicate predicateWithFormat:@"%K == $PRIMARY_KEY_VALUE", primaryKeyAttribute] : nil; objc_setAssociatedObject(self, &primaryKeyPredicateKey, predicate, @@ -27,10 +27,25 @@ static char primaryKeyAttributeKey, primaryKeyPredicateKey; #pragma mark - Public -- (NSString *)primaryKeyAttribute +- (NSAttributeDescription *)primaryKeyAttribute +{ + return [[self attributesByName] valueForKey:[self primaryKeyAttributeName]]; +} + +- (Class)primaryKeyAttributeClass +{ + NSAttributeDescription *attributeDescription = [self primaryKeyAttribute]; + if (attributeDescription) { + return NSClassFromString(attributeDescription.attributeValueClassName); + } + + return nil; +} + +- (NSString *)primaryKeyAttributeName { // Check for an associative object reference - NSString *primaryKeyAttribute = (NSString *) objc_getAssociatedObject(self, &primaryKeyAttributeKey); + NSString *primaryKeyAttribute = (NSString *) objc_getAssociatedObject(self, &primaryKeyAttributeNameKey); // Fall back to the userInfo dictionary if (! primaryKeyAttribute) { @@ -45,16 +60,15 @@ static char primaryKeyAttributeKey, primaryKeyPredicateKey; return primaryKeyAttribute; } -- (void)setPrimaryKeyAttribute:(NSString *)primaryKeyAttribute +- (void)setPrimaryKeyAttributeName:(NSString *)primaryKeyAttributeName { objc_setAssociatedObject(self, - &primaryKeyAttributeKey, - primaryKeyAttribute, + &primaryKeyAttributeNameKey, + primaryKeyAttributeName, OBJC_ASSOCIATION_RETAIN); - [self setPredicateForPrimaryKeyAttribute:primaryKeyAttribute]; + [self setPredicateForPrimaryKeyAttribute:primaryKeyAttributeName]; } - - (NSPredicate *)predicateForPrimaryKeyAttribute { return (NSPredicate *) objc_getAssociatedObject(self, &primaryKeyPredicateKey); @@ -62,7 +76,23 @@ static char primaryKeyAttributeKey, primaryKeyPredicateKey; - (NSPredicate *)predicateForPrimaryKeyAttributeWithValue:(id)value { - NSDictionary *variables = [NSDictionary dictionaryWithObject:value + id searchValue = value; + Class theClass = [self primaryKeyAttributeClass]; + if (theClass) { + // TODO: This coercsion behave should be pluggable and reused from the mapper + if ([theClass isSubclassOfClass:[NSNumber class]] && ![searchValue isKindOfClass:[NSNumber class]]) { + // Handle NSString -> NSNumber + if ([searchValue isKindOfClass:[NSString class]]) { + searchValue = [NSNumber numberWithDouble:[searchValue doubleValue]]; + } + } else if ([theClass isSubclassOfClass:[NSString class]] && ![searchValue isKindOfClass:[NSString class]]) { + // Coerce to string + if ([searchValue respondsToSelector:@selector(stringValue)]) { + searchValue = [searchValue stringValue]; + } + } + } + NSDictionary *variables = [NSDictionary dictionaryWithObject:searchValue forKey:RKEntityDescriptionPrimaryKeyAttributeValuePredicateSubstitutionVariable]; return [[self predicateForPrimaryKeyAttribute] predicateWithSubstitutionVariables:variables]; } diff --git a/Code/CoreData/NSManagedObject+ActiveRecord.m b/Code/CoreData/NSManagedObject+ActiveRecord.m index f8de2ca6..a246f020 100644 --- a/Code/CoreData/NSManagedObject+ActiveRecord.m +++ b/Code/CoreData/NSManagedObject+ActiveRecord.m @@ -138,14 +138,13 @@ RK_FIX_CATEGORY_BUG(NSManagedObject_ActiveRecord) } + (id)findByPrimaryKey:(id)primaryKeyValue inContext:(NSManagedObjectContext *)context { - NSEntityDescription *entity = [self entityDescriptionInContext:context]; - NSString *primaryKeyAttribute = entity.primaryKeyAttribute; - if (! primaryKeyAttribute) { - RKLogWarning(@"Attempt to findByPrimaryKey for entity with nil primaryKeyAttribute. Set the primaryKeyAttribute and try again! %@", entity); + NSPredicate *predicate = [[self entityDescriptionInContext:context] predicateForPrimaryKeyAttributeWithValue:primaryKeyValue]; + if (! predicate) { + RKLogWarning(@"Attempt to findByPrimaryKey for entity with nil primaryKeyAttribute. Set the primaryKeyAttributeName and try again! %@", self); return nil; } - - return [self findFirstByAttribute:primaryKeyAttribute withValue:primaryKeyValue inContext:context]; + + return [self findFirstWithPredicate:predicate inContext:context]; } + (id)findByPrimaryKey:(id)primaryKeyValue { diff --git a/Code/CoreData/RKFetchRequestManagedObjectCache.m b/Code/CoreData/RKFetchRequestManagedObjectCache.m index 4ef30182..3409a039 100644 --- a/Code/CoreData/RKFetchRequestManagedObjectCache.m +++ b/Code/CoreData/RKFetchRequestManagedObjectCache.m @@ -41,7 +41,7 @@ // Use cached predicate if primary key matches NSPredicate *predicate = nil; - if ([entity.primaryKeyAttribute isEqualToString:primaryKeyAttribute]) { + if ([entity.primaryKeyAttributeName isEqualToString:primaryKeyAttribute]) { predicate = [entity predicateForPrimaryKeyAttributeWithValue:searchValue]; } else { // Parse a predicate diff --git a/Code/CoreData/RKManagedObjectMapping.m b/Code/CoreData/RKManagedObjectMapping.m index 16c92bce..58e6b01d 100644 --- a/Code/CoreData/RKManagedObjectMapping.m +++ b/Code/CoreData/RKManagedObjectMapping.m @@ -202,17 +202,17 @@ } /* - Allows the primaryKeyAttribute property on the NSEntityDescription to configure the mapping and vice-versa + Allows the primaryKeyAttributeName property on the NSEntityDescription to configure the mapping and vice-versa */ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"entity"]) { if (! self.primaryKeyAttribute) { - self.primaryKeyAttribute = [self.entity primaryKeyAttribute]; + self.primaryKeyAttribute = [self.entity primaryKeyAttributeName]; } } else if ([keyPath isEqualToString:@"primaryKeyAttribute"]) { if (! self.entity.primaryKeyAttribute) { - self.entity.primaryKeyAttribute = self.primaryKeyAttribute; + self.entity.primaryKeyAttributeName = self.primaryKeyAttribute; } } } diff --git a/Tests/Logic/CoreData/NSEntityDescription+RKAdditionsTest.m b/Tests/Logic/CoreData/NSEntityDescription+RKAdditionsTest.m index 4faca0d9..4d8080de 100644 --- a/Tests/Logic/CoreData/NSEntityDescription+RKAdditionsTest.m +++ b/Tests/Logic/CoreData/NSEntityDescription+RKAdditionsTest.m @@ -19,7 +19,7 @@ { RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:objectStore.primaryManagedObjectContext]; - assertThat(entity.primaryKeyAttribute, is(equalTo(@"railsID"))); + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"railsID"))); } - (void)testRetrievalOfUnconfiguredPrimaryKeyAttributeReturnsNil @@ -29,28 +29,28 @@ assertThat(entity.primaryKeyAttribute, is(nilValue())); } -- (void)testSettingPrimaryKeyAttributeProgramatically +- (void)testSettingPrimaryKeyAttributeNameProgramatically { RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKHouse" inManagedObjectContext:objectStore.primaryManagedObjectContext]; - entity.primaryKeyAttribute = @"houseID"; - assertThat(entity.primaryKeyAttribute, is(equalTo(@"houseID"))); + entity.primaryKeyAttributeName = @"houseID"; + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"houseID"))); } -- (void)testSettingExistingPrimaryKeyAttributeProgramatically +- (void)testSettingExistingPrimaryKeyAttributeNameProgramatically { RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:objectStore.primaryManagedObjectContext]; - assertThat(entity.primaryKeyAttribute, is(equalTo(@"railsID"))); - entity.primaryKeyAttribute = @"catID"; - assertThat(entity.primaryKeyAttribute, is(equalTo(@"catID"))); + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"railsID"))); + entity.primaryKeyAttributeName = @"catID"; + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"catID"))); } - (void)testSettingPrimaryKeyAttributeCreatesCachedPredicate { RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:objectStore.primaryManagedObjectContext]; - assertThat(entity.primaryKeyAttribute, is(equalTo(@"railsID"))); + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"railsID"))); assertThat([entity.predicateForPrimaryKeyAttribute predicateFormat], is(equalTo(@"railsID == $PRIMARY_KEY_VALUE"))); } @@ -58,10 +58,83 @@ { RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:objectStore.primaryManagedObjectContext]; - assertThat(entity.primaryKeyAttribute, is(equalTo(@"railsID"))); + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"railsID"))); NSNumber *primaryKeyValue = [NSNumber numberWithInt:12345]; NSPredicate *predicate = [entity predicateForPrimaryKeyAttributeWithValue:primaryKeyValue]; assertThat([predicate predicateFormat], is(equalTo(@"railsID == 12345"))); } +- (void)testThatPredicateForPrimaryKeyAttributeCastsStringValueToNumber +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"railsID"))); + NSPredicate *predicate = [entity predicateForPrimaryKeyAttributeWithValue:@"12345"]; + assertThat([predicate predicateFormat], is(equalTo(@"railsID == 12345"))); +} + +- (void)testThatPredicateForPrimaryKeyAttributeCastsNumberToString +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKHouse" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + entity.primaryKeyAttributeName = @"city"; + NSPredicate *predicate = [entity predicateForPrimaryKeyAttributeWithValue:[NSNumber numberWithInteger:12345]]; + assertThat([predicate predicateFormat], is(equalTo(@"city == \"12345\""))); +} + +- (void)testThatPredicateForPrimaryKeyAttributeReturnsNilForEntityWithoutPrimaryKey +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKHouse" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + entity.primaryKeyAttributeName = nil; + NSPredicate *predicate = [entity predicateForPrimaryKeyAttributeWithValue:@"12345"]; + assertThat([predicate predicateFormat], is(nilValue())); +} + +- (void)testRetrievalOfPrimaryKeyAttributeReturnsNilIfNotSet +{ + NSEntityDescription *entity = [NSEntityDescription new]; + assertThat(entity.primaryKeyAttribute, is(nilValue())); +} + +- (void)testRetrievalOfPrimaryKeyAttributeReturnsNilWhenSetToInvalidAttributeName +{ + NSEntityDescription *entity = [NSEntityDescription new]; + entity.primaryKeyAttributeName = @"invalidName!"; + assertThat(entity.primaryKeyAttribute, is(nilValue())); +} + +- (void)testRetrievalOfPrimaryKeyAttributeForValidAttributeName +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + entity.primaryKeyAttributeName = @"railsID"; + NSAttributeDescription *attribute = entity.primaryKeyAttribute; + assertThat(attribute, is(notNilValue())); + assertThat(attribute.name, is(equalTo(@"railsID"))); + assertThat(attribute.attributeValueClassName, is(equalTo(@"NSNumber"))); +} + +- (void)testRetrievalOfPrimaryKeyAttributeClassReturnsNilIfNotSet +{ + NSEntityDescription *entity = [NSEntityDescription new]; + assertThat([entity primaryKeyAttributeClass], is(nilValue())); +} + +- (void)testRetrievalOfPrimaryKeyAttributeClassReturnsNilWhenSetToInvalidAttributeName +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKHouse" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + entity.primaryKeyAttributeName = @"invalid"; + assertThat([entity primaryKeyAttributeClass], is(nilValue())); +} + +- (void)testRetrievalOfPrimaryKeyAttributeClassForValidAttributeName +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKHouse" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + entity.primaryKeyAttributeName = @"railsID"; + assertThat([entity primaryKeyAttributeClass], is(equalTo([NSNumber class]))); +} + @end diff --git a/Tests/Logic/CoreData/NSManagedObject+ActiveRecordTest.m b/Tests/Logic/CoreData/NSManagedObject+ActiveRecordTest.m index 1dfa94fd..7c5c1a4c 100644 --- a/Tests/Logic/CoreData/NSManagedObject+ActiveRecordTest.m +++ b/Tests/Logic/CoreData/NSManagedObject+ActiveRecordTest.m @@ -20,7 +20,7 @@ { RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; NSEntityDescription *entity = [RKHuman entityDescription]; - entity.primaryKeyAttribute = @"railsID"; + entity.primaryKeyAttributeName = @"railsID"; RKHuman *human = [RKHuman createEntity]; human.railsID = [NSNumber numberWithInt:12345]; @@ -35,7 +35,7 @@ RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; NSManagedObjectContext *context = [[RKTestFactory managedObjectStore] newManagedObjectContext]; NSEntityDescription *entity = [RKHuman entityDescription]; - entity.primaryKeyAttribute = @"railsID"; + entity.primaryKeyAttributeName = @"railsID"; RKHuman *human = [RKHuman createInContext:context]; human.railsID = [NSNumber numberWithInt:12345]; @@ -48,4 +48,18 @@ assertThat(foundHuman, is(equalTo(human))); } +- (void)testFindByPrimaryKeyWithStringValueForNumericProperty +{ + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [RKHuman entityDescription]; + entity.primaryKeyAttributeName = @"railsID"; + + RKHuman *human = [RKHuman createEntity]; + human.railsID = [NSNumber numberWithInt:12345]; + [store save:nil]; + + RKHuman *foundHuman = [RKHuman findByPrimaryKey:@"12345" inContext:store.primaryManagedObjectContext]; + assertThat(foundHuman, is(equalTo(human))); +} + @end diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingTest.m index 8e367779..25b45c24 100644 --- a/Tests/Logic/CoreData/RKManagedObjectMappingTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectMappingTest.m @@ -175,8 +175,9 @@ NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCloud" inManagedObjectContext:store.primaryManagedObjectContext]; RKManagedObjectMapping *mapping = [RKManagedObjectMapping mappingForEntity:entity inManagedObjectStore:store]; assertThat(mapping.primaryKeyAttribute, is(nilValue())); - mapping.primaryKeyAttribute = @"cloudID"; - assertThat(entity.primaryKeyAttribute, is(equalTo(@"cloudID"))); + mapping.primaryKeyAttribute = @"name"; + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"name"))); + assertThat(entity.primaryKeyAttribute, is(notNilValue())); } #pragma mark - Fetched Results Cache @@ -271,7 +272,7 @@ [RKHuman truncateAll]; RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; mapping.primaryKeyAttribute = @"name"; - [RKHuman entity].primaryKeyAttribute = @"railsID"; + [RKHuman entity].primaryKeyAttributeName = @"railsID"; [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"monkey.name" toKeyPath:@"name"]]; [RKHuman truncateAll]; @@ -297,7 +298,7 @@ [RKHuman truncateAll]; RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; mapping.primaryKeyAttribute = @"name"; - [RKHuman entity].primaryKeyAttribute = @"railsID"; + [RKHuman entity].primaryKeyAttributeName = @"railsID"; [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"monkey.name" toKeyPath:@"name"]]; [RKHuman truncateAll]; From 992bfb96cc382924e86aaa588f6c5c6fe5e2fdaf Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Wed, 23 May 2012 16:35:56 -0400 Subject: [PATCH 07/12] Reworked table controller state into a bit mask to coalesce state change into a single observation. fixes #753 * Added RKLogIntegerAsBinary() helper for logging bit masks * Implemented RKTableControllerDidLoadObjectsNotification for static and fetched results table controllers * Cleaned up state definitions within table controller * Documentation cleanups --- Code/Support/RKLog.h | 6 + Code/Support/RKLog.m | 10 + Code/UI/RKAbstractTableController.h | 353 +++++++---- Code/UI/RKAbstractTableController.m | 599 ++++++++---------- Code/UI/RKAbstractTableController_Internals.h | 42 +- Code/UI/RKFetchedResultsTableController.h | 2 +- Code/UI/RKFetchedResultsTableController.m | 34 +- Code/UI/RKTableController.h | 47 +- Code/UI/RKTableController.m | 166 ++++- .../UI/RKFetchedResultsTableControllerTest.m | 17 + Tests/Application/UI/RKTableControllerTest.m | 207 +++++- 11 files changed, 1011 insertions(+), 472 deletions(-) diff --git a/Code/Support/RKLog.h b/Code/Support/RKLog.h index 794a9aa9..14113986 100644 --- a/Code/Support/RKLog.h +++ b/Code/Support/RKLog.h @@ -205,3 +205,9 @@ void RKLogConfigureFromEnvironment(void); of a failed key-value validation error. */ void RKLogValidationError(NSError *); + +/** + Logs the value of an NSUInteger as a binary string. Useful when + examining integers containing bitmasked values. + */ +void RKLogIntegerAsBinary(NSUInteger); diff --git a/Code/Support/RKLog.m b/Code/Support/RKLog.m index 71dc34d2..ca5b06c9 100644 --- a/Code/Support/RKLog.m +++ b/Code/Support/RKLog.m @@ -159,3 +159,13 @@ void RKLogValidationError(NSError *validationError) { } } } + +void RKLogIntegerAsBinary(NSUInteger bitMask) { + NSUInteger bit = ~(NSUIntegerMax >> 1); + NSMutableString *string = [NSMutableString string]; + do { + [string appendString:(((NSUInteger)bitMask & bit) ? @"1" : @"0")]; + } while ( bit >>= 1 ); + + NSLog(@"Value of %ld in binary: %@", (long) bitMask, string); +} diff --git a/Code/UI/RKAbstractTableController.h b/Code/UI/RKAbstractTableController.h index cac5699f..088bf112 100755 --- a/Code/UI/RKAbstractTableController.h +++ b/Code/UI/RKAbstractTableController.h @@ -21,60 +21,102 @@ #if TARGET_OS_IPHONE #import -#import "RKTableSection.h" #import "RKTableViewCellMappings.h" #import "RKTableItem.h" #import "RKObjectManager.h" #import "RKObjectMapping.h" #import "RKObjectLoader.h" -/** @name Constants */ - -/** Posted when the table view model starts loading */ -extern NSString* const RKTableControllerDidStartLoadNotification; - -/** Posted when the table view model finishes loading */ -extern NSString* const RKTableControllerDidFinishLoadNotification; - -/** Posted when the table view model has loaded objects into the table view */ -extern NSString* const RKTableControllerDidLoadObjectsNotification; - -/** Posted when the table view model has loaded an empty collection of objects into the table view */ -extern NSString* const RKTableControllerDidLoadEmptyNotification; - -/** Posted when the table view model has loaded an error */ -extern NSString* const RKTableControllerDidLoadErrorNotification; - -/** Posted when the table view model has transitioned from offline to online */ -extern NSString* const RKTableControllerDidBecomeOnline; - -/** Posted when the table view model has transitioned from online to offline */ -extern NSString* const RKTableControllerDidBecomeOffline; - -@protocol RKTableControllerDelegate; +///----------------------------------------------------------------------------- +/// @name Constants +///----------------------------------------------------------------------------- /** - RestKit's table view abstraction leverages the object mapping engine to transform - local objects into UITableViewCell representations. The table view model encapsulates - the functionality of a UITableView dataSource and delegate into a single reusable - component. + Posted when the table controller starts loading. */ -@interface RKAbstractTableController : NSObject { - @protected - UIView *_tableOverlayView; - UIImageView *_stateOverlayImageView; - UIView *_pullToRefreshHeaderView; - RKCache *_cache; -} +extern NSString * const RKTableControllerDidStartLoadNotification; -///////////////////////////////////////////////////////////////////////// +/** + Posted when the table controller finishes loading. + */ +extern NSString * const RKTableControllerDidFinishLoadNotification; + +/** + Posted when the table controller has loaded objects into the table view. + */ +extern NSString * const RKTableControllerDidLoadObjectsNotification; + +/** + Posted when the table controller has loaded an empty collection of objects into the table view. + */ +extern NSString * const RKTableControllerDidLoadEmptyNotification; + +/** + Posted when the table controller has loaded an error. + */ +extern NSString * const RKTableControllerDidLoadErrorNotification; + +/** + Posted when the table controller has transitioned from an offline to online state. + */ +extern NSString * const RKTableControllerDidBecomeOnline; + +/** + Posted when the table controller has transitioned from an online to an offline state. + */ +extern NSString * const RKTableControllerDidBecomeOffline; + +@protocol RKAbstractTableControllerDelegate; + +/** + @enum RKTableControllerState + + @constant RKTableControllerStateNormal Indicates that the table has + loaded normally and is displaying cell content. It is not loading content, + is not empty, has not loaded an error, and is not offline. + + @constant RKTableControllerStateLoading Indicates that the table controller + is loading content from a remote source. + + @constant RKTableControllerStateEmpty Indicates that the table controller has + retrieved an empty collection of objects. + + @constant RKTableControllerStateError Indicates that the table controller has + encountered an error while attempting to load. + + @constant RKTableControllerStateOffline Indicates that the table controller is + offline and cannot perform network access. + + @constant RKTableControllerStateNotYetLoaded Indicates that the table controller is + has not yet attempted a load and state is unknown. + */ +enum RKTableControllerState { + RKTableControllerStateNormal = 0, + RKTableControllerStateLoading = 1 << 1, + RKTableControllerStateEmpty = 1 << 2, + RKTableControllerStateError = 1 << 3, + RKTableControllerStateOffline = 1 << 4, + RKTableControllerStateNotYetLoaded = 0xFF000000 +}; +typedef NSUInteger RKTableControllerState; + +/** + RKAbstractTableController is an abstract base class for concrete table controller classes. + A table controller object acts as both the delegate and data source for a UITableView + object and leverages the RestKit object mapping engine to transform local domain models + into UITableViewCell representations. Concrete implementations are provided for the + display of static table views and Core Data backed fetched results controller basied + table views. + */ +@interface RKAbstractTableController : NSObject + +///----------------------------------------------------------------------------- /// @name Configuring the Table Controller -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- -@property (nonatomic, assign) id delegate; -@property (nonatomic, readonly) UIViewController* viewController; -@property (nonatomic, readonly) UITableView* tableView; -@property (nonatomic, readonly) NSMutableArray* sections; +@property (nonatomic, assign) id delegate; +@property (nonatomic, readonly) UIViewController *viewController; +@property (nonatomic, readonly) UITableView *tableView; @property (nonatomic, assign) UITableViewRowAnimation defaultRowAnimation; @property (nonatomic, assign) BOOL pullToRefreshEnabled; @@ -82,28 +124,28 @@ extern NSString* const RKTableControllerDidBecomeOffline; @property (nonatomic, assign) BOOL canMoveRows; @property (nonatomic, assign) BOOL autoResizesForKeyboard; -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /// @name Instantiation -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- -+ (id)tableControllerWithTableView:(UITableView*)tableView - forViewController:(UIViewController*)viewController; ++ (id)tableControllerWithTableView:(UITableView *)tableView + forViewController:(UIViewController *)viewController; -+ (id)tableControllerForTableViewController:(UITableViewController*)tableViewController; ++ (id)tableControllerForTableViewController:(UITableViewController *)tableViewController; -- (id)initWithTableView:(UITableView*)tableView - viewController:(UIViewController*)viewController; +- (id)initWithTableView:(UITableView *)tableView + viewController:(UIViewController *)viewController; -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /// @name Object to Table View Cell Mappings -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- -@property (nonatomic, retain) RKTableViewCellMappings* cellMappings; +@property (nonatomic, retain) RKTableViewCellMappings *cellMappings; -- (void)mapObjectsWithClass:(Class)objectClass toTableCellsWithMapping:(RKTableViewCellMapping*)cellMapping; -- (void)mapObjectsWithClassName:(NSString *)objectClassName toTableCellsWithMapping:(RKTableViewCellMapping*)cellMapping; +- (void)mapObjectsWithClass:(Class)objectClass toTableCellsWithMapping:(RKTableViewCellMapping *)cellMapping; +- (void)mapObjectsWithClassName:(NSString *)objectClassName toTableCellsWithMapping:(RKTableViewCellMapping *)cellMapping; - (id)objectForRowAtIndexPath:(NSIndexPath *)indexPath; -- (RKTableViewCellMapping*)cellMappingForObjectAtIndexPath:(NSIndexPath *)indexPath; +- (RKTableViewCellMapping *)cellMappingForObjectAtIndexPath:(NSIndexPath *)indexPath; /** Return the index path of the object within the table @@ -111,9 +153,9 @@ extern NSString* const RKTableControllerDidBecomeOffline; - (NSIndexPath *)indexPathForObject:(id)object; - (UITableViewCell *)cellForObject:(id)object; -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /// @name Header and Footer Rows -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- - (void)addHeaderRowForItem:(RKTableItem *)tableItem; - (void)addFooterRowForItem:(RKTableItem *)tableItem; @@ -122,12 +164,12 @@ extern NSString* const RKTableControllerDidBecomeOffline; - (void)removeAllHeaderRows; - (void)removeAllFooterRows; -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /// @name RESTful Table Loading -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /** - The object manager instance this table view model is associated with. + The object manager instance this table controller is associated with. This instance is used for creating object loaders when loading Network tables and provides the managed object store used for Core Data tables. @@ -144,42 +186,102 @@ extern NSString* const RKTableControllerDidBecomeOffline; - (void)cancelLoad; - (BOOL)isAutoRefreshNeeded; -///////////////////////////////////////////////////////////////////////// -/// @name Model State Views -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- +/// @name Inspecting Table State +///----------------------------------------------------------------------------- +/** + The current state of the table controller. Note that the controller may be in more + than one state (e.g. loading | empty). + */ +@property (nonatomic, readonly, assign) RKTableControllerState state; + +/** + An error object that was encountered as the result of an attempt to load + the table. Will return a value when the table is in the error state, + otherwise nil. + */ +@property (nonatomic, readonly, retain) NSError *error; + +/** + Returns a Boolean value indicating if the table controller is currently + loading content. + */ - (BOOL)isLoading; + +/** + Returns a Boolean value indicating if the table controller has attempted + a load and transitioned into any state. + */ - (BOOL)isLoaded; + +/** + Returns a Boolean value indicating if the table controller has loaded an + empty set of content. + + When YES and there is not an empty item configured, the table controller + will optionally display an empty image overlayed on top of the table view. + + **NOTE**: It is possible for an empty table controller to display cells + witin the managed table view in the event an empty item or header/footer + rows are configured. + + @see imageForEmpty + */ - (BOOL)isEmpty; + +/** + Returns a Boolean value indicating if the table controller is online + and network operations may be performed. + */ - (BOOL)isOnline; -@property (nonatomic, readonly) BOOL isError; -@property (nonatomic, readonly, retain) NSError* error; +/** + Returns a Boolean value indicating if the table controller is offline. + + When YES, the table controller will optionally display an offline image + overlayed on top of the table view. + + @see imageForOffline + */ +- (BOOL)isOffline; + +/** + Returns a Boolean value indicating if the table controller encountered + an error while attempting to load. + + When YES, the table controller will optionally display an error image + overlayed on top of the table view. + + @see imageForError + */ +- (BOOL)isError; + +///----------------------------------------------------------------------------- +/// @name Model State Views +///----------------------------------------------------------------------------- /** An image to overlay onto the table when the table view does not have any row data to display. It will be centered - within the table view + within the table view. */ -// TODO: Should be emptyImage -@property (nonatomic, retain) UIImage* imageForEmpty; +@property (nonatomic, retain) UIImage *imageForEmpty; /** An image to overlay onto the table when a load operation has encountered an error. It will be centered within the table view. */ -// TODO: Should be errorImage -@property (nonatomic, retain) UIImage* imageForError; +@property (nonatomic, retain) UIImage *imageForError; /** An image to overlay onto the table with when the user does - not have connectivity to the Internet + not have connectivity to the Internet. @see RKReachabilityObserver */ -// TODO: Should be offlineImage -@property (nonatomic, retain) UIImage* imageForOffline; +@property (nonatomic, retain) UIImage *imageForOffline; /** A UIView to add to the table overlay during loading. It @@ -187,7 +289,19 @@ extern NSString* const RKTableControllerDidBecomeOffline; The loading view is always presented non-modally. */ -@property (nonatomic, retain) UIView* loadingView; +@property (nonatomic, retain) UIView *loadingView; + +/** + Returns the image, if any, configured for display when the table controller + is in the given state. + + **NOTE** This method accepts a single state value. + + @param state The table controller state + @return The image for the specified state, else nil. Always returns nil for + RKTableControllerStateNormal, RKTableControllerStateLoading and RKTableControllerStateLoading. + */ +- (UIImage *)imageForState:(RKTableControllerState)state; /** A rectangle configuring the dimensions for the overlay view that is @@ -200,6 +314,11 @@ extern NSString* const RKTableControllerDidBecomeOffline; */ @property (nonatomic, assign) CGRect overlayFrame; +/** + The image currently displayed within the overlay view. + */ +@property (nonatomic, readonly) UIImage *overlayImage; + /** When YES, the image view added to the table overlay for displaying table state (i.e. for offline, error and empty) will be displayed modally @@ -213,54 +332,47 @@ extern NSString* const RKTableControllerDidBecomeOffline; @property (nonatomic, assign) BOOL variableHeightRows; @property (nonatomic, assign) BOOL showsHeaderRowsWhenEmpty; @property (nonatomic, assign) BOOL showsFooterRowsWhenEmpty; -@property (nonatomic, retain) RKTableItem* emptyItem; +@property (nonatomic, retain) RKTableItem *emptyItem; -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /// @name Managing Sections -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- -/** The number of sections in the model. */ +/** + The number of sections in the table. + */ @property (nonatomic, readonly) NSUInteger sectionCount; -/** The number of rows across all sections in the model. */ +/** + The number of rows across all sections in the model. + */ @property (nonatomic, readonly) NSUInteger rowCount; -/** Returns the section at the specified index. - * @param index Must be less than the total number of sections. */ -- (RKTableSection *)sectionAtIndex:(NSUInteger)index; - -/** Returns the first section with the specified header title. - * @param title The header title. */ -- (RKTableSection *)sectionWithHeaderTitle:(NSString *)title; - /** Returns the number of rows in the section at the given index. @param index The index of the section to return the row count for. - @returns The + @returns The number of rows contained within the section with the given index. @raises NSInvalidArgumentException Raised if index is greater than or - equal to the total number of sections in the table. + equal to the total number of sections in the table. */ -- (NSUInteger)numberOfRowsInSectionAtIndex:(NSUInteger)index; +- (NSUInteger)numberOfRowsInSection:(NSUInteger)index; -/** Returns the index of the specified section. - * @param section Must be a valid non nil RKTableViewSection. - * @return If section is not found, method returns NSNotFound. */ -- (NSUInteger)indexForSection:(RKTableSection *)section; - -/** Returns the UITableViewCell created by applying the specified - * mapping operation to the object identified by indexPath. - * @param indexPath The indexPath in the tableView for which a cell - * is needed. */ +/** + Returns the UITableViewCell created by applying the specified + mapping operation to the object identified by indexPath. + + @param indexPath The indexPath in the tableView for which a cell is needed. + */ - (UITableViewCell *)cellForObjectAtIndexPath:(NSIndexPath *)indexPath; -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /// @name Managing Swipe View -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- @property (nonatomic, assign) BOOL cellSwipeViewsEnabled; -@property (nonatomic, retain) UIView* cellSwipeView; -@property (nonatomic, readonly) UITableViewCell* swipeCell; +@property (nonatomic, retain) UIView *cellSwipeView; +@property (nonatomic, readonly) UITableViewCell *swipeCell; @property (nonatomic, readonly) id swipeObject; @property (nonatomic, readonly) BOOL animatingCellSwipe; @property (nonatomic, readonly) UISwipeGestureRecognizerDirection swipeDirection; @@ -270,45 +382,50 @@ extern NSString* const RKTableControllerDidBecomeOffline; @end -@protocol RKTableControllerDelegate +@protocol RKAbstractTableControllerDelegate @optional // Network -- (void)tableController:(RKAbstractTableController *)tableController willLoadTableWithObjectLoader:(RKObjectLoader*)objectLoader; -- (void)tableController:(RKAbstractTableController *)tableController didLoadTableWithObjectLoader:(RKObjectLoader*)objectLoader; +- (void)tableController:(RKAbstractTableController *)tableController willLoadTableWithObjectLoader:(RKObjectLoader *)objectLoader; +- (void)tableController:(RKAbstractTableController *)tableController didLoadTableWithObjectLoader:(RKObjectLoader *)objectLoader; // Basic States - (void)tableControllerDidStartLoad:(RKAbstractTableController *)tableController; -/** Sent when the table view has transitioned out of the loading state regardless of outcome **/ +/** + Sent when the table view has transitioned out of the loading state regardless of outcome + */ - (void)tableControllerDidFinishLoad:(RKAbstractTableController *)tableController; -- (void)tableController:(RKAbstractTableController *)tableController didFailLoadWithError:(NSError*)error; +- (void)tableController:(RKAbstractTableController *)tableController didFailLoadWithError:(NSError *)error; - (void)tableControllerDidCancelLoad:(RKAbstractTableController *)tableController; -- (void)tableController:(RKAbstractTableController *)tableController didLoadObjects:(NSArray*)objects inSection:(NSUInteger)sectionIndex; +- (void)tableController:(RKAbstractTableController *)tableController didLoadObjects:(NSArray *)objects inSection:(NSUInteger)sectionIndex; // NOT IMPLEMENTED -/** Sent to the delegate when the controller is really and truly finished loading/updating, whether from the network or from Core Data, or from static data, ... this happens in didFinishLoading - **/ +/** + Sent to the delegate when the controller is really and truly finished loading/updating, whether from the network or from Core Data, + or from static data, ... this happens in didFinishLoading + */ - (void)tableControllerDidFinalizeLoad:(RKAbstractTableController *)tableController; /** Sent to the delegate when the content of the table view has become empty */ -- (void)tableControllerDidBecomeEmpty:(RKAbstractTableController *)tableController; // didLoadEmpty??? +- (void)tableControllerDidBecomeEmpty:(RKAbstractTableController *)tableController; /** - Sent to the delegate when the table view model has transitioned from offline to online + Sent to the delegate when the table controller has transitioned from offline to online */ - (void)tableControllerDidBecomeOnline:(RKAbstractTableController *)tableController; /** - Sent to the delegate when the table view model has transitioned from online to offline + Sent to the delegate when the table controller has transitioned from online to offline */ - (void)tableControllerDidBecomeOffline:(RKAbstractTableController *)tableController; // Sections -- (void)tableController:(RKAbstractTableController *)tableController didInsertSection:(RKTableSection *)section atIndex:(NSUInteger)sectionIndex; -- (void)tableController:(RKAbstractTableController *)tableController didRemoveSection:(RKTableSection *)section atIndex:(NSUInteger)sectionIndex; +// TODO: Can these even be implemented??? +- (void)tableController:(RKAbstractTableController *)tableController didInsertSectionAtIndex:(NSUInteger)sectionIndex; +- (void)tableController:(RKAbstractTableController *)tableController didRemoveSectionAtIndex:(NSUInteger)sectionIndex; // Objects - (void)tableController:(RKAbstractTableController *)tableController didInsertObject:(id)object atIndexPath:(NSIndexPath *)indexPath; @@ -320,8 +437,8 @@ extern NSString* const RKTableControllerDidBecomeOffline; - (void)tableController:(RKAbstractTableController *)tableController didEndEditing:(id)object atIndexPath:(NSIndexPath *)indexPath; // Swipe Views -- (void)tableController:(RKAbstractTableController *)tableController willAddSwipeView:(UIView*)swipeView toCell:(UITableViewCell *)cell forObject:(id)object; -- (void)tableController:(RKAbstractTableController *)tableController willRemoveSwipeView:(UIView*)swipeView fromCell:(UITableViewCell *)cell forObject:(id)object; +- (void)tableController:(RKAbstractTableController *)tableController willAddSwipeView:(UIView *)swipeView toCell:(UITableViewCell *)cell forObject:(id)object; +- (void)tableController:(RKAbstractTableController *)tableController willRemoveSwipeView:(UIView *)swipeView fromCell:(UITableViewCell *)cell forObject:(id)object; // BELOW NOT YET IMPLEMENTED diff --git a/Code/UI/RKAbstractTableController.m b/Code/UI/RKAbstractTableController.m index 5d9e91b4..fefc8cfd 100755 --- a/Code/UI/RKAbstractTableController.m +++ b/Code/UI/RKAbstractTableController.m @@ -26,6 +26,7 @@ #import "RKReachabilityObserver.h" #import "UIView+FindFirstResponder.h" #import "RKRefreshGestureRecognizer.h" +#import "RKTableSection.h" // Define logging component #undef RKLogComponent @@ -37,22 +38,21 @@ */ #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"; +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"; +static NSString * lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; @implementation RKAbstractTableController @synthesize delegate = _delegate; @synthesize viewController = _viewController; @synthesize tableView = _tableView; -@synthesize sections = _sections; @synthesize defaultRowAnimation = _defaultRowAnimation; @synthesize objectLoader = _objectLoader; @@ -61,10 +61,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; @synthesize autoRefreshFromNetwork = _autoRefreshFromNetwork; @synthesize autoRefreshRate = _autoRefreshRate; -@synthesize empty = _empty; -@synthesize loading = _loading; -@synthesize loaded = _loaded; -@synthesize online = _online; +@synthesize state = _state; @synthesize error = _error; @synthesize imageForEmpty = _imageForEmpty; @@ -95,20 +92,21 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; @synthesize tableOverlayView = _tableOverlayView; @synthesize stateOverlayImageView = _stateOverlayImageView; @synthesize cache = _cache; +@synthesize pullToRefreshHeaderView = _pullToRefreshHeaderView; #pragma mark - Instantiation -+ (id)tableControllerWithTableView:(UITableView*)tableView - forViewController:(UIViewController*)viewController { ++ (id)tableControllerWithTableView:(UITableView *)tableView + forViewController:(UIViewController *)viewController { return [[[self alloc] initWithTableView:tableView viewController:viewController] autorelease]; } -+ (id)tableControllerForTableViewController:(UITableViewController*)tableViewController { ++ (id)tableControllerForTableViewController:(UITableViewController *)tableViewController { return [self tableControllerWithTableView:tableViewController.tableView forViewController:tableViewController]; } -- (id)initWithTableView:(UITableView*)theTableView viewController:(UIViewController*)theViewController { +- (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]; @@ -134,7 +132,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; userInfo:nil]; } - _sections = [NSMutableArray new]; + self.state = RKTableControllerStateNotYetLoaded; self.objectManager = [RKObjectManager sharedManager]; _cellMappings = [RKTableViewCellMappings new]; @@ -149,25 +147,13 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; // 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" + forKeyPath:@"state" 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; } @@ -187,11 +173,8 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; _tableOverlayView = nil; // Remove observers - [self removeObserver:self forKeyPath:@"loading"]; - [self removeObserver:self forKeyPath:@"loaded"]; - [self removeObserver:self forKeyPath:@"empty"]; + [self removeObserver:self forKeyPath:@"state"]; [self removeObserver:self forKeyPath:@"error"]; - [self removeObserver:self forKeyPath:@"online"]; [[NSNotificationCenter defaultCenter] removeObserver:self]; // TODO: WTF? Get UI crashes when enabled... @@ -199,8 +182,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; _objectLoader.delegate = nil; [_objectLoader release]; _objectLoader = nil; - - [_sections release]; + [_cellMappings release]; [_headerItems release]; [_footerItems release]; @@ -254,7 +236,11 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; // Initialize online/offline state (if it is known) if (objectManager.networkStatus != RKObjectManagerNetworkStatusUnknown) { - self.online = objectManager.isOnline; + if (objectManager.isOnline) { + self.state &= ~RKTableControllerStateOffline; + } else { + self.state |= RKTableControllerStateOffline; + } } } } @@ -282,7 +268,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; if (_autoRefreshFromNetwork != autoRefreshFromNetwork) { _autoRefreshFromNetwork = autoRefreshFromNetwork; if (_autoRefreshFromNetwork) { - NSString* cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] + NSString *cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"RKAbstractTableControllerCache"]; _cache = [[RKCache alloc] initWithPath:cachePath subDirectories:nil]; } else { @@ -295,130 +281,110 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; } } -- (void)objectManagerConnectivityDidChange:(NSNotification *)notification { - RKLogTrace(@"%@ received network status change notification: %@", self, [notification name]); - self.online = self.objectManager.isOnline; +- (void)setLoading:(BOOL)loading { + if (loading) { + self.state |= RKTableControllerStateLoading; + } else { + self.state &= ~RKTableControllerStateLoading; + } } -#pragma mark - Managing Sections +// NOTE: The loaded flag is handled specially. When loaded becomes NO, +// we clear all other flags. In practice this should not happen outside of init. +- (void)setLoaded:(BOOL)loaded { + if (loaded) { + self.state &= ~RKTableControllerStateNotYetLoaded; + } else { + self.state = RKTableControllerStateNotYetLoaded; + } +} + +- (void)setEmpty:(BOOL)empty { + if (empty) { + self.state |= RKTableControllerStateEmpty; + } else { + self.state &= ~RKTableControllerStateEmpty; + } +} + +- (void)setOffline:(BOOL)offline { + if (offline) { + self.state |= RKTableControllerStateOffline; + } else { + self.state &= ~RKTableControllerStateOffline; + } +} + +- (void)setErrorState:(BOOL)error { + if (error) { + self.state |= RKTableControllerStateError; + } else { + self.state &= ~RKTableControllerStateError; + } +} + +- (void)objectManagerConnectivityDidChange:(NSNotification *)notification { + RKLogTrace(@"%@ received network status change notification: %@", self, [notification name]); + [self setOffline:!self.objectManager.isOnline]; +} + +#pragma mark - Abstract Methods + +- (BOOL)isConsideredEmpty { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; +} - (NSUInteger)sectionCount { - return [_sections count]; + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; } - (NSUInteger)rowCount { - return [[_sections valueForKeyPath:@"@sum.rowCount"] intValue]; + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; } -- (RKTableSection *)sectionAtIndex:(NSUInteger)index { - return [_sections objectAtIndex:index]; +- (id)objectForRowAtIndexPath:(NSIndexPath *)indexPath { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; } -- (NSUInteger)indexForSection:(RKTableSection *)section { - NSAssert(section, @"Cannot return index for a nil section"); - return [_sections indexOfObject:section]; +- (NSIndexPath *)indexPathForObject:(id)object { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; } -- (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; +- (NSUInteger)numberOfRowsInSection:(NSUInteger)index { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo: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; + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; } #pragma mark - Cell Mappings -- (void)mapObjectsWithClass:(Class)objectClass toTableCellsWithMapping:(RKTableViewCellMapping*)cellMapping { +- (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 { +- (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 { +- (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]; @@ -429,41 +395,24 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; 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 { +- (void)addHeaderRowForItem:(RKTableItem *)tableItem { [_headerItems addObject:tableItem]; } -- (void)addFooterRowForItem:(RKTableItem*)tableItem { +- (void)addFooterRowForItem:(RKTableItem *)tableItem { [_footerItems addObject:tableItem]; } - (void)addHeaderRowWithMapping:(RKTableViewCellMapping *)cellMapping { - RKTableItem* tableItem = [RKTableItem tableItem]; + RKTableItem *tableItem = [RKTableItem tableItem]; tableItem.cellMapping = cellMapping; [self addHeaderRowForItem:tableItem]; } - (void)addFooterRowWithMapping:(RKTableViewCellMapping *)cellMapping { - RKTableItem* tableItem = [RKTableItem tableItem]; + RKTableItem *tableItem = [RKTableItem tableItem]; tableItem.cellMapping = cellMapping; [self addFooterRowForItem:tableItem]; } @@ -476,17 +425,32 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; [_footerItems removeAllObjects]; } +#pragma mark - UITableViewDataSource methods + +- (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; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + [NSException raise:@"Must be implemented in a subclass!" format:@"sectionCount must be implemented with a subclass"]; + return 0; +} + #pragma mark - UITableViewDelegate methods -- (void)tableView:(UITableView*)theTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { +- (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]; + 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 @@ -508,7 +472,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; 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]; + RKTableViewCellMapping *cellMapping = [self.cellMappings cellMappingForObject:mappableObject]; if (cellMapping.onCellWillAppearForObjectAtIndexPath) { cellMapping.onCellWillAppearForObjectAtIndexPath(cell, mappableObject, indexPath); } @@ -540,7 +504,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; - (CGFloat)tableView:(UITableView *)theTableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if (self.variableHeightRows) { - RKTableViewCellMapping* cellMapping = [self cellMappingForObjectAtIndexPath:indexPath]; + RKTableViewCellMapping *cellMapping = [self cellMappingForObjectAtIndexPath:indexPath]; if (cellMapping.heightOfCellForObjectAtIndexPath) { id object = [self objectForRowAtIndexPath:indexPath]; @@ -557,55 +521,31 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; 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]; +- (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]; + 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]; +- (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]; + 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 { +- (UITableViewCellEditingStyle)tableView:(UITableView *)theTableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { if (_canEditRows) { - RKTableViewCellMapping* cellMapping = [self cellMappingForObjectAtIndexPath:indexPath]; - UITableViewCell* cell = [self tableView:self.tableView cellForRowAtIndexPath:indexPath]; + 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]; @@ -616,26 +556,26 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; return UITableViewCellEditingStyleNone; } -- (void)tableView:(UITableView*)theTableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath { +- (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 { +- (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 { +- (NSIndexPath *)tableView:(UITableView *)theTableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath { if (_canMoveRows) { - RKTableViewCellMapping* cellMapping = [self cellMappingForObjectAtIndexPath:sourceIndexPath]; + 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]; + UITableViewCell *cell = [self tableView:self.tableView cellForRowAtIndexPath:sourceIndexPath]; id object = [self objectForRowAtIndexPath:sourceIndexPath]; return cellMapping.targetIndexPathForMove(cell, object, sourceIndexPath, proposedDestinationIndexPath); } @@ -643,7 +583,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; return proposedDestinationIndexPath; } -- (NSIndexPath *)tableView:(UITableView*)theTableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath { +- (NSIndexPath *)tableView:(UITableView *)theTableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath { [self removeSwipeView:YES]; return indexPath; } @@ -654,18 +594,18 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; [self.objectLoader cancel]; } -- (NSDate*)lastUpdatedDate { +- (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]; + 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]; + NSString *absoluteURLString = [self.objectLoader.URL absoluteString]; + NSNumber *lastUpdatedTimeIntervalSince1970 = (NSNumber *)[lastUpdatedDates objectForKey:absoluteURLString]; if (absoluteURLString && lastUpdatedTimeIntervalSince1970) { return [NSDate dateWithTimeIntervalSince1970:[lastUpdatedTimeIntervalSince1970 doubleValue]]; } @@ -678,7 +618,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; BOOL isAutoRefreshNeeded = NO; if (_autoRefreshFromNetwork) { isAutoRefreshNeeded = YES; - NSDate* lastUpdatedDate = [self lastUpdatedDate]; + NSDate *lastUpdatedDate = [self lastUpdatedDate]; RKLogTrace(@"Last updated: %@", lastUpdatedDate); if (lastUpdatedDate) { RKLogTrace(@"-timeIntervalSinceNow=%f, autoRefreshRate=%f", @@ -691,12 +631,12 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; #pragma mark - RKRequestDelegate & RKObjectLoaderDelegate methods -- (void)requestDidStartLoad:(RKRequest*)request { +- (void)requestDidStartLoad:(RKRequest *)request { RKLogTrace(@"tableController %@ started loading.", self); self.loading = YES; } -- (void)requestDidCancelLoad:(RKRequest*)request { +- (void)requestDidCancelLoad:(RKRequest *)request { RKLogTrace(@"tableController %@ cancelled loading.", self); self.loading = NO; @@ -705,7 +645,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; } } -- (void)requestDidTimeout:(RKRequest*)request { +- (void)requestDidTimeout:(RKRequest *)request { RKLogTrace(@"tableController %@ timed out while loading.", self); self.loading = NO; } @@ -716,13 +656,13 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; // 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]; + NSMutableDictionary *lastUpdatedDates = [[_cache dictionaryForCacheKey:lastUpdatedDateDictionaryKey] mutableCopy]; if (lastUpdatedDates) { [_cache invalidateEntry:lastUpdatedDateDictionaryKey]; } else { lastUpdatedDates = [[NSMutableDictionary alloc] init]; } - NSNumber* timeIntervalSince1970 = [NSNumber numberWithDouble:[[NSDate date] timeIntervalSince1970]]; + NSNumber *timeIntervalSince1970 = [NSNumber numberWithDouble:[[NSDate date] timeIntervalSince1970]]; RKLogTrace(@"Setting timeIntervalSince1970=%@ for URL %@", timeIntervalSince1970, [request.URL absoluteString]); [lastUpdatedDates setObject:timeIntervalSince1970 forKey:[request.URL absoluteString]]; @@ -747,15 +687,10 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; } - (void)didFinishLoad { - self.empty = [self isEmpty]; + self.empty = [self isConsideredEmpty]; 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(tableControllerDidFinalizeLoad:)]) { [_delegate performSelector:@selector(tableControllerDidFinalizeLoad:) withObject:self]; } @@ -763,6 +698,36 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; #pragma mark - Table Overlay Views +- (UIImage *)imageForState:(RKTableControllerState)state { + switch (state) { + case RKTableControllerStateNormal: + case RKTableControllerStateLoading: + case RKTableControllerStateNotYetLoaded: + break; + + case RKTableControllerStateEmpty: + return self.imageForEmpty; + break; + + case RKTableControllerStateError: + return self.imageForError; + break; + + case RKTableControllerStateOffline: + return self.imageForOffline; + break; + + default: + break; + } + + return nil; +} + +- (UIImage *)overlayImage { + return _stateOverlayImageView.image; +} + // Adds an overlay view above the table - (void)addToOverlayView:(UIView *)view modally:(BOOL)modally { if (! _tableOverlayView) { @@ -835,7 +800,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; [self resetOverlayView]; } -- (void)setImageForEmpty:(UIImage*)imageForEmpty { +- (void)setImageForEmpty:(UIImage *)imageForEmpty { [imageForEmpty retain]; BOOL imageRemoved = [self removeImageFromOverlay:_imageForEmpty]; [_imageForEmpty release]; @@ -843,7 +808,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; if (imageRemoved) [self showImageInOverlay:_imageForEmpty]; } -- (void)setImageForError:(UIImage*)imageForError { +- (void)setImageForError:(UIImage *)imageForError { [imageForError retain]; BOOL imageRemoved = [self removeImageFromOverlay:_imageForError]; [_imageForError release]; @@ -851,7 +816,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; if (imageRemoved) [self showImageInOverlay:_imageForError]; } -- (void)setImageForOffline:(UIImage*)imageForOffline { +- (void)setImageForOffline:(UIImage *)imageForOffline { [imageForOffline retain]; BOOL imageRemoved = [self removeImageFromOverlay:_imageForOffline]; [_imageForOffline release]; @@ -859,7 +824,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; if (imageRemoved) [self showImageInOverlay:_imageForOffline]; } -- (void)setLoadingView:(UIView*)loadingView { +- (void)setLoadingView:(UIView *)loadingView { [loadingView retain]; BOOL viewRemoved = (_loadingView.superview != nil); [_loadingView removeFromSuperview]; @@ -869,41 +834,34 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; if (viewRemoved) [self addToOverlayView:_loadingView modally:NO]; } -#pragma mark - KVO & Model States +#pragma mark - KVO & Table States - (BOOL)isLoading { - return self.loading; + return (self.state & RKTableControllerStateLoading) != 0; } - (BOOL)isLoaded { - return self.loaded; + return (self.state & RKTableControllerStateNotYetLoaded) == 0; +// return self.state != RKTableControllerStateNotYetLoaded; } +- (BOOL)isOffline { + return (self.state & RKTableControllerStateOffline) != 0; +} - (BOOL)isOnline { - return self.online; + return ![self isOffline]; } - (BOOL)isError { - return _error != nil; + return (self.state & RKTableControllerStateError) != 0; } - (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; + return (self.state & RKTableControllerStateEmpty) != 0; } -- (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; - +- (void)isLoadingDidChange { + if ([self isLoading]) { if ([self.delegate respondsToSelector:@selector(tableControllerDidStartLoad:)]) { [self.delegate tableControllerDidStartLoad:self]; } @@ -929,65 +887,40 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; } // We don't want any image overlays applied until loading is finished - _stateOverlayImageView.hidden = isLoading; + _stateOverlayImageView.hidden = [self isLoading]; } -- (void)isLoadedDidChangeTo:(BOOL)isLoaded { - if (isLoaded) { +- (void)isLoadedDidChange { + if ([self isLoaded]) { RKLogDebug(@"%@: is now loaded.", self); } else { RKLogDebug(@"%@: is NOT loaded.", self); } } -- (void)errorDidChangeTo:(BOOL)isError { - if (isError) { +- (void)isErrorDidChange { + if ([self isError]) { if ([self.delegate respondsToSelector:@selector(tableController:didFailLoadWithError:)]) { [self.delegate tableController:self didFailLoadWithError:self.error]; } - NSDictionary* userInfo = [NSDictionary dictionaryWithObject:self.error forKey:RKErrorNotificationErrorKey]; + 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? +- (void)isEmptyDidChange { + if ([self isEmpty]) { 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) { +- (void)isOnlineDidChange { + if ([self isOnline]) { // We just transitioned to online if ([self.delegate respondsToSelector:@selector(tableControllerDidBecomeOnline:)]) { [self.delegate tableControllerDidBecomeOnline:self]; @@ -1001,37 +934,59 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; } [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidBecomeOffline object:self]; - } + } +} - [self updateOfflineImageForOnlineState:isOnline]; +- (void)updateTableViewForStateChange:(NSDictionary *)change { + RKTableControllerState oldState = [[change valueForKey:NSKeyValueChangeOldKey] integerValue]; + RKTableControllerState newState = [[change valueForKey:NSKeyValueChangeNewKey] integerValue]; + + // Determine state transitions + BOOL loadedChanged = ((oldState ^ newState) & RKTableControllerStateNotYetLoaded); + BOOL emptyChanged = ((oldState ^ newState) & RKTableControllerStateEmpty); + BOOL offlineChanged = ((oldState ^ newState) & RKTableControllerStateOffline); + BOOL loadingChanged = ((oldState ^ newState) & RKTableControllerStateLoading); + BOOL errorChanged = ((oldState ^ newState) & RKTableControllerStateError); + + if (loadedChanged) [self isLoadedDidChange]; + if (emptyChanged) [self isEmptyDidChange]; + if (offlineChanged) [self isOnlineDidChange]; + if (errorChanged) [self isErrorDidChange]; + if (loadingChanged) [self isLoadingDidChange]; + + // Clear the image from the overlay + _stateOverlayImageView.image = nil; + + // Determine the appropriate overlay image to display (if any) + if (self.state == RKTableControllerStateNormal) { + [self removeImageOverlay]; + } else { + if ([self isLoading]) { + // During a load we don't adjust the overlay + return; + } + + // Though the table can be in more than one state, we only + // want to display a single overlay image. + if ([self isOffline] && self.imageForOffline) { + [self showImageInOverlay:self.imageForOffline]; + } else if ([self isError] && self.imageForError) { + [self showImageInOverlay:self.imageForError]; + } else if ([self isEmpty] && self.imageForEmpty) { + [self showImageInOverlay:self.imageForEmpty]; + } + } + + // Remove the overlay if no longer in use + [self resetOverlayView]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - 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]; + if ([keyPath isEqualToString:@"state"]) { + [self updateTableViewForStateChange:change]; } 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]; + [self setErrorState:(self.error != nil)]; } - - RKLogTrace(@"Key-value observation triggered for keyPath '%@'. Old value = %d, new value = %d", keyPath, oldValue, newValue); } #pragma mark - Pull to Refresh @@ -1074,18 +1029,18 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; } - (void)resetPullToRefreshRecognizer { - RKRefreshGestureRecognizer* recognizer = [self pullToRefreshGestureRecognizer]; + RKRefreshGestureRecognizer *recognizer = [self pullToRefreshGestureRecognizer]; if (recognizer) [recognizer setRefreshState:RKRefreshIdle]; } -- (BOOL)pullToRefreshDataSourceIsLoading:(UIGestureRecognizer*)gesture { +- (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]; +- (NSDate *)pullToRefreshDataSourceLastUpdated:(UIGestureRecognizer *)gesture { + NSDate *dataSourceLastUpdated = [self lastUpdatedDate]; return dataSourceLastUpdated ? dataSourceLastUpdated : [NSDate date]; } @@ -1093,20 +1048,20 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; - (void)setupSwipeGestureRecognizers { // Setup a right swipe gesture recognizer - UISwipeGestureRecognizer* rightSwipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRight:)]; + 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:)]; + 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) { + for (UIGestureRecognizer *recognizer in self.tableView.gestureRecognizers) { if ([recognizer isKindOfClass:[UISwipeGestureRecognizer class]]) { [self.tableView removeGestureRecognizer:recognizer]; } @@ -1129,11 +1084,11 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; _cellSwipeViewsEnabled = cellSwipeViewsEnabled; } -- (void)swipe:(UISwipeGestureRecognizer*)recognizer direction:(UISwipeGestureRecognizerDirection)direction { +- (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]; + NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location]; + UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; id object = [self objectForRowAtIndexPath:indexPath]; if (cell.frame.origin.x != 0) { @@ -1149,11 +1104,11 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; } } -- (void)swipeLeft:(UISwipeGestureRecognizer*)recognizer { +- (void)swipeLeft:(UISwipeGestureRecognizer *)recognizer { [self swipe:recognizer direction:UISwipeGestureRecognizerDirectionLeft]; } -- (void)swipeRight:(UISwipeGestureRecognizer*)recognizer { +- (void)swipeRight:(UISwipeGestureRecognizer *)recognizer { [self swipe:recognizer direction:UISwipeGestureRecognizerDirectionRight]; } @@ -1192,7 +1147,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; } } -- (void)animationDidStopAddingSwipeView:(NSString*)animationID finished:(NSNumber*)finished context:(void*)context { +- (void)animationDidStopAddingSwipeView:(NSString *)animationID finished:(NSNumber *)finished context:(void*)context { _animatingCellSwipe = NO; } @@ -1230,7 +1185,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; } } -- (void)animationDidStopOne:(NSString*)animationID finished:(NSNumber*)finished context:(void*)context { +- (void)animationDidStopOne:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.2]; if (_swipeDirection == UISwipeGestureRecognizerDirectionRight) { @@ -1244,7 +1199,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; [UIView commitAnimations]; } -- (void)animationDidStopTwo:(NSString*)animationID finished:(NSNumber*)finished context:(void*)context { +- (void)animationDidStopTwo:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context { [UIView commitAnimations]; [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.2]; @@ -1259,7 +1214,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; [UIView commitAnimations]; } -- (void)animationDidStopThree:(NSString*)animationID finished:(NSNumber*)finished context:(void*)context { +- (void)animationDidStopThree:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context { _animatingCellSwipe = NO; [_swipeCell release]; _swipeCell = nil; @@ -1268,7 +1223,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; #pragma mark UIScrollViewDelegate methods -- (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView { +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { [self removeSwipeView:YES]; } @@ -1279,9 +1234,9 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; #pragma mark - Keyboard Notification methods -- (void)resizeTableViewForKeyboard:(NSNotification*)notification { +- (void)resizeTableViewForKeyboard:(NSNotification *)notification { NSAssert(_autoResizesForKeyboard, @"Errantly receiving keyboard notifications while autoResizesForKeyboard=NO"); - NSDictionary* userInfo = [notification userInfo]; + NSDictionary *userInfo = [notification userInfo]; CGRect keyboardEndFrame = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGFloat heightForViewShift = keyboardEndFrame.size.height; @@ -1289,13 +1244,13 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; keyboardEndFrame.size.height, heightForViewShift); CGFloat bottomBarOffset = 0.0; - UINavigationController* navigationController = self.viewController.navigationController; + 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; + 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); @@ -1314,7 +1269,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; nonKeyboardRect.origin.x, nonKeyboardRect.origin.y, nonKeyboardRect.size.width, nonKeyboardRect.size.height); - UIView* firstResponder = [self.tableView findFirstResponder]; + UIView *firstResponder = [self.tableView findFirstResponder]; if (firstResponder) { CGRect firstResponderFrame = firstResponder.frame; RKLogTrace(@"Found firstResponder=%@ at (%f, %f, %f, %f)", firstResponder, @@ -1345,7 +1300,7 @@ static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; } } -- (void)loadTableWithObjectLoader:(RKObjectLoader*)theObjectLoader { +- (void)loadTableWithObjectLoader:(RKObjectLoader *)theObjectLoader { NSAssert(theObjectLoader, @"Cannot perform a network load without an object loader"); if (! [self.objectLoader isEqual:theObjectLoader]) { if (self.objectLoader) { diff --git a/Code/UI/RKAbstractTableController_Internals.h b/Code/UI/RKAbstractTableController_Internals.h index 023952ef..ef6a7828 100644 --- a/Code/UI/RKAbstractTableController_Internals.h +++ b/Code/UI/RKAbstractTableController_Internals.h @@ -18,25 +18,25 @@ // limitations under the License. // -#import +#import #import "RKRefreshGestureRecognizer.h" +/* + A private continuation class for subclass implementations of RKAbstractTableController + */ @interface RKAbstractTableController () -@property (nonatomic, readwrite, assign) UITableView* tableView; -@property (nonatomic, readwrite, assign) UIViewController* viewController; -@property (nonatomic, readwrite, retain) RKObjectLoader* objectLoader; -@property (nonatomic, readwrite, assign) BOOL loading; -@property (nonatomic, readwrite, assign) BOOL loaded; -@property (nonatomic, readwrite, assign) BOOL empty; -@property (nonatomic, readwrite, assign) BOOL online; -@property (nonatomic, readwrite, retain) NSError* error; -@property (nonatomic, readwrite, retain) NSMutableArray* headerItems; -@property (nonatomic, readwrite, retain) NSMutableArray* footerItems; - +@property (nonatomic, readwrite, assign) UITableView *tableView; +@property (nonatomic, readwrite, assign) UIViewController *viewController; +@property (nonatomic, assign, readwrite) RKTableControllerState state; +@property (nonatomic, readwrite, retain) RKObjectLoader *objectLoader; +@property (nonatomic, readwrite, retain) NSError *error; +@property (nonatomic, readwrite, retain) NSMutableArray *headerItems; +@property (nonatomic, readwrite, retain) NSMutableArray *footerItems; @property (nonatomic, readonly) UIView *tableOverlayView; @property (nonatomic, readonly) UIImageView *stateOverlayImageView; @property (nonatomic, readonly) RKCache *cache; +@property (nonatomic, retain) UIView *pullToRefreshHeaderView; /** Must be invoked when the table controller has finished loading. @@ -45,7 +45,6 @@ and cleaning up the table overlay view. */ - (void)didFinishLoad; -- (void)updateOfflineImageForOnlineState:(BOOL)isOnline; #pragma mark - Table View Overlay @@ -61,5 +60,22 @@ - (void)pullToRefreshStateChanged:(UIGestureRecognizer *)gesture; - (void)resetPullToRefreshRecognizer; +#pragma mark - State Mutators + +- (void)setLoading:(BOOL)loading; +- (void)setLoaded:(BOOL)loaded; +- (void)setEmpty:(BOOL)empty; +- (void)setOffline:(BOOL)offline; +- (void)setErrorState:(BOOL)error; + +/** + Returns a Boolean value indicating if the table controller + should be considered empty and transitioned into the empty state. + Used by the abstract table controller to trigger state transitions. + + **NOTE**: This is an abstract method that MUST be implemented with + a subclass. + */ +- (BOOL)isConsideredEmpty; @end diff --git a/Code/UI/RKFetchedResultsTableController.h b/Code/UI/RKFetchedResultsTableController.h index c56a5f69..adf837d8 100755 --- a/Code/UI/RKFetchedResultsTableController.h +++ b/Code/UI/RKFetchedResultsTableController.h @@ -33,7 +33,7 @@ typedef UIView *(^RKFetchedResultsTableViewViewForHeaderInSectionBlock)(NSUInteg BOOL _isEmptyBeforeAnimation; } -@property (nonatomic, readonly) NSFetchedResultsController *fetchedResultsController; +@property (nonatomic, retain, readonly) NSFetchedResultsController *fetchedResultsController; @property (nonatomic, copy) NSString *resourcePath; @property (nonatomic, retain) NSFetchRequest *fetchRequest; @property (nonatomic, assign) CGFloat heightForHeaderInSection; diff --git a/Code/UI/RKFetchedResultsTableController.m b/Code/UI/RKFetchedResultsTableController.m index 8d8c89f9..09f5e4fb 100755 --- a/Code/UI/RKFetchedResultsTableController.m +++ b/Code/UI/RKFetchedResultsTableController.m @@ -31,6 +31,8 @@ #define RKLogComponent lcl_cRestKitUI @interface RKFetchedResultsTableController () +@property (nonatomic, retain, readwrite) NSFetchedResultsController *fetchedResultsController; + - (void)performFetch; - (void)updateSortedArray; @end @@ -86,6 +88,7 @@ RKLogError(@"performFetch failed with error: %@", [error localizedDescription]); } else { RKLogTrace(@"performFetch completed successfully"); + [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadObjectsNotification object:self]; } } @@ -226,17 +229,15 @@ if (_sortDescriptors) { [fetchRequest setSortDescriptors:_sortDescriptors]; } - - [_fetchedResultsController setDelegate:nil]; - [_fetchedResultsController release]; - _fetchedResultsController = nil; - - _fetchedResultsController = + + self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[NSManagedObjectContext contextForCurrentThread] sectionNameKeyPath:_sectionNameKeyPath cacheName:_cacheName]; + [_fetchedResultsController release]; _fetchedResultsController.delegate = self; + [self performFetch]; [self updateSortedArray]; @@ -470,7 +471,7 @@ return self.emptyItem; } else if ([self isHeaderIndexPath:indexPath]) { - NSUInteger row = (self.empty && self.emptyItem) ? (indexPath.row - 1) : indexPath.row; + NSUInteger row = ([self isEmpty] && self.emptyItem) ? (indexPath.row - 1) : indexPath.row; return [self.headerItems objectAtIndex:row]; } else if ([self isFooterIndexPath:indexPath]) { @@ -499,7 +500,7 @@ #pragma mark - KVO & Model States -- (BOOL)isEmpty { +- (BOOL)isConsideredEmpty { NSUInteger fetchedObjectsCount = [[_fetchedResultsController fetchedObjects] count]; BOOL isEmpty = (fetchedObjectsCount == 0); RKLogTrace(@"Determined isEmpty = %@. fetchedObjects count = %d", isEmpty ? @"YES" : @"NO", fetchedObjectsCount); @@ -599,8 +600,23 @@ } else { [self.tableView endUpdates]; } - + + [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadObjectsNotification object:self]; [self didFinishLoad]; } +#pragma mark - UITableViewDataSource methods + +- (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; +} + +- (NSUInteger)numberOfRowsInSection:(NSUInteger)index { + return [self tableView:self.tableView numberOfRowsInSection:index]; +} + @end diff --git a/Code/UI/RKTableController.h b/Code/UI/RKTableController.h index a6b11450..87e4f734 100644 --- a/Code/UI/RKTableController.h +++ b/Code/UI/RKTableController.h @@ -29,11 +29,22 @@ #import "RKObjectMapping.h" #import "RKObjectLoader.h" +@protocol RKTableControllerDelegate + +@optional + +- (void)tableController:(RKAbstractTableController *)tableController didInsertSection:(RKTableSection *)section atIndex:(NSUInteger)sectionIndex; +- (void)tableController:(RKAbstractTableController *)tableController didRemoveSection:(RKTableSection *)section atIndex:(NSUInteger)sectionIndex; + +@end + @interface RKTableController : RKAbstractTableController -///////////////////////////////////////////////////////////////////////// +@property (nonatomic, assign) id delegate; + +///----------------------------------------------------------------------------- /// @name Static Tables -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- - (void)loadObjects:(NSArray *)objects; - (void)loadObjects:(NSArray *)objects inSection:(NSUInteger)sectionIndex; @@ -78,12 +89,16 @@ */ - (void)loadTableItems:(NSArray *)tableItems inSection:(NSUInteger)sectionIndex; +///----------------------------------------------------------------------------- /** @name Network Tables */ +///----------------------------------------------------------------------------- - (void)loadTableFromResourcePath:(NSString *)resourcePath; - (void)loadTableFromResourcePath:(NSString *)resourcePath usingBlock:(void (^)(RKObjectLoader *objectLoader))block; +///----------------------------------------------------------------------------- /** @name Forms */ +///----------------------------------------------------------------------------- /** The form that the table has been loaded with (if any) @@ -98,15 +113,39 @@ */ - (void)loadForm:(RKForm *)form; -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /// @name Managing Sections -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- + +@property (nonatomic, readonly) NSMutableArray *sections; /** The key path on the loaded objects used to determine the section they belong to. */ @property(nonatomic, copy) NSString *sectionNameKeyPath; +/** + Returns the section at the specified index. + @param index Must be less than the total number of sections. + */ +- (RKTableSection *)sectionAtIndex:(NSUInteger)index; + +/** + Returns the first section with the specified header title. + @param title The header title. + */ +// MOVED +- (RKTableSection *)sectionWithHeaderTitle:(NSString *)title; + +/** + Returns the index of the specified section. + + @param section Must be a valid non nil RKTableViewSection. + @return The index of the given section if contained within the receiver, otherwise NSNotFound. + */ +// MOVED +- (NSUInteger)indexForSection:(RKTableSection *)section; + // Coalesces a series of table view updates performed within the block into // a single animation using beginUpdates: and endUpdates: on the table view // TODO: Move to super-class? diff --git a/Code/UI/RKTableController.m b/Code/UI/RKTableController.m index 0d892d60..f7d6d257 100644 --- a/Code/UI/RKTableController.m +++ b/Code/UI/RKTableController.m @@ -23,27 +23,35 @@ #import "RKLog.h" #import "RKFormSection.h" #import "NSArray+RKAdditions.h" +#import "RKObjectMappingOperation.h" // Define logging component #undef RKLogComponent #define RKLogComponent lcl_cRestKitUI +@interface RKTableController () +@property (nonatomic, readwrite) NSMutableArray *sections; +@end + @implementation RKTableController +@dynamic delegate; @synthesize form = _form; @synthesize sectionNameKeyPath = _sectionNameKeyPath; +@synthesize sections = _sections; #pragma mark - Instantiation - (id)init { self = [super init]; if (self) { + _sections = [NSMutableArray new]; [self addObserver:self forKeyPath:@"sections" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; - RKTableSection* section = [RKTableSection section]; + RKTableSection *section = [RKTableSection section]; [self addSection:section]; } @@ -54,7 +62,8 @@ [self removeObserver:self forKeyPath:@"sections"]; [_form release]; [_sectionNameKeyPath release]; - + [_sections release]; + [super dealloc]; } @@ -194,7 +203,9 @@ } [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:self.defaultRowAnimation]; - + + [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadObjectsNotification object:self]; + // The load is finalized via network callbacks for // dynamic table controllers if (nil == self.objectLoader) { @@ -376,4 +387,153 @@ // TODO: KVO should be used for managing the row level manipulations on the table view as well... } +#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)numberOfRowsInSection:(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 - Cell Mappings + +- (id)objectForRowAtIndexPath:(NSIndexPath *)indexPath { + NSAssert(indexPath, @"Cannot lookup object with a nil indexPath"); + RKTableSection* section = [self sectionAtIndex:indexPath.section]; + return [section objectAtIndex:indexPath.row]; +} + +#pragma mark - UITableViewDataSource methods + +- (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 self.canEditRows; +} + +- (BOOL)tableView:(UITableView*)theTableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { + NSAssert(theTableView == self.tableView, @"tableView:canMoveRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView); + return self.canMoveRows; +} + +- (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]; +} + +- (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; +} + +- (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; +} + +- (BOOL)isConsideredEmpty { + NSUInteger nonRowItemsCount = [self.headerItems count] + [self.footerItems count]; + nonRowItemsCount += self.emptyItem ? 1 : 0; + BOOL isEmpty = (self.rowCount - nonRowItemsCount) == 0; + RKLogTrace(@"Determined isConsideredEmpty = %@. self.rowCount = %d with %d nonRowItems in the table", isEmpty ? @"YES" : @"NO", self.rowCount, nonRowItemsCount); + return isEmpty; +} + @end diff --git a/Tests/Application/UI/RKFetchedResultsTableControllerTest.m b/Tests/Application/UI/RKFetchedResultsTableControllerTest.m index aca2c59a..220f7903 100644 --- a/Tests/Application/UI/RKFetchedResultsTableControllerTest.m +++ b/Tests/Application/UI/RKFetchedResultsTableControllerTest.m @@ -1386,4 +1386,21 @@ assertThat(tableController.stateOverlayImageView.image, is(notNilValue())); } +- (void)testPostANotificationWhenObjectsAreLoaded { + [self bootstrapNakedObjectStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/NakedEvents.json"; + [tableController setObjectMappingForClass:[RKEvent class]]; + + id observerMock = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidLoadObjectsNotification object:tableController]; + [[observerMock expect] notificationWithName:RKTableControllerDidLoadObjectsNotification object:tableController]; + [tableController loadTable]; + [observerMock verify]; +} + @end diff --git a/Tests/Application/UI/RKTableControllerTest.m b/Tests/Application/UI/RKTableControllerTest.m index 72677057..6e21c5cb 100644 --- a/Tests/Application/UI/RKTableControllerTest.m +++ b/Tests/Application/UI/RKTableControllerTest.m @@ -119,7 +119,7 @@ //////////////////////////////////////////////////////////////////////////////////////////////////////////////// -@interface RKTableControllerTest : SenTestCase +@interface RKTableControllerTest : RKTestCase @end @@ -151,6 +151,12 @@ assertThat(tableController.tableView, is(equalTo(tableView))); } +- (void)testInitializesToUnloadedState { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); +} + - (void)testAlwaysHaveAtLeastOneSection { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; assertThat(viewController.tableView, is(notNilValue())); @@ -671,8 +677,10 @@ - (void)testAllowYouToTriggerAnEmptyLoad { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKLogIntegerAsBinary(tableController.state); assertThatBool([tableController isLoaded], is(equalToBool(NO))); [tableController loadEmpty]; + RKLogIntegerAsBinary(tableController.state); assertThatBool([tableController isLoaded], is(equalToBool(YES))); assertThatBool([tableController isEmpty], is(equalToBool(YES))); } @@ -726,6 +734,71 @@ assertThatBool([tableController isEmpty], is(equalToBool(YES))); } +- (void)testErrorIsClearedAfterSubsequentLoad { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + id mockObjectLoader = [OCMockObject niceMockForClass:[RKObjectLoader class]]; + NSError* error = [NSError errorWithDomain:@"Test" code:0 userInfo:nil]; + [tableController objectLoader:mockObjectLoader didFailWithError:error]; + assertThatBool([tableController isLoading], is(equalToBool(NO))); + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatBool([tableController isError], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + + [tableController objectLoader:mockObjectLoader didLoadObjects:[NSArray array]]; + assertThatBool([tableController isError], is(equalToBool(NO))); + assertThat(tableController.error, is(nilValue())); +} + +- (void)testDisplayOfErrorImageTakesPresendenceOverEmpty { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + UIImage *imageForEmpty = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; + UIImage *imageForError = [imageForEmpty copy]; + tableController.imageForEmpty = imageForEmpty; + tableController.imageForError = imageForError; + + id mockObjectLoader = [OCMockObject niceMockForClass:[RKObjectLoader class]]; + NSError* error = [NSError errorWithDomain:@"Test" code:0 userInfo:nil]; + [tableController objectLoader:mockObjectLoader didFailWithError:error]; + assertThatBool([tableController isLoading], is(equalToBool(NO))); + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatBool([tableController isError], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + + UIImage *overlayImage = [tableController overlayImage]; + assertThat(overlayImage, isNot(nilValue())); + assertThat(overlayImage, is(equalTo(imageForError))); +} + +- (void)testBitwiseLoadingTransition { + RKTableControllerState oldState = RKTableControllerStateNotYetLoaded; + RKTableControllerState newState = RKTableControllerStateLoading; + + BOOL loadingTransitioned = ((oldState ^ newState) & RKTableControllerStateLoading); + assertThatBool(loadingTransitioned, is(equalToBool(YES))); + + oldState = RKTableControllerStateOffline | RKTableControllerStateEmpty; + newState = RKTableControllerStateOffline | RKTableControllerStateEmpty | RKTableControllerStateLoading; + loadingTransitioned = ((oldState ^ newState) & RKTableControllerStateLoading); + assertThatBool(loadingTransitioned, is(equalToBool(YES))); + + oldState = RKTableControllerStateNormal; + newState = RKTableControllerStateLoading; + loadingTransitioned = ((oldState ^ newState) & RKTableControllerStateLoading); + assertThatBool(loadingTransitioned, is(equalToBool(YES))); + + oldState = RKTableControllerStateOffline | RKTableControllerStateEmpty | RKTableControllerStateLoading; + newState = RKTableControllerStateOffline | RKTableControllerStateLoading; + loadingTransitioned = ((oldState ^ newState) & RKTableControllerStateLoading); + assertThatBool(loadingTransitioned, is(equalToBool(NO))); + + oldState = RKTableControllerStateNotYetLoaded; + newState = RKTableControllerStateOffline; + loadingTransitioned = ((oldState ^ newState) & RKTableControllerStateLoading); + assertThatBool(loadingTransitioned, is(equalToBool(NO))); +} + - (void)testSetTheModelToAnEmptyStateIfTheObjectLoaderReturnsAnEmptyCollection { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; @@ -1164,6 +1237,32 @@ [observerMock verify]; } +- (void)testPostANotificationWhenObjectsAreLoaded { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = objectManager; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { + mapping.cellClass = [RKTestUserTableViewCell class]; + [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; + }]]; + + id observerMock = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidLoadObjectsNotification object:tableController]; + [[observerMock expect] notificationWithName:RKTableControllerDidLoadObjectsNotification object:tableController]; + RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + tableController.delegate = delegate; + + [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [delegate waitForLoad]; + [observerMock verify]; +} + - (void)testPostANotificationWhenAnErrorOccurs { RKObjectManager* objectManager = [RKTestFactory objectManager]; RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; @@ -1216,6 +1315,110 @@ [observerMock verify]; } +#pragma mark - State Transitions + +- (void)testInitializesToNotYetLoadedState { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool(tableController.state == RKTableControllerStateNotYetLoaded, is(equalToBool(YES))); +} + +- (void)testInitialLoadSetsStateToLoading { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); + assertThatBool([tableController isLoading], is(equalToBool(NO))); + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + [tableController requestDidStartLoad:mockLoader]; + assertThatBool([tableController isLoading], is(equalToBool(YES))); + assertThatBool([tableController isLoaded], is(equalToBool(NO))); +} + +- (void)testSuccessfulLoadSetsStateToNormal { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); + assertThatBool([tableController isLoading], is(equalToBool(NO))); + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + [tableController objectLoader:mockLoader didLoadObjects:[NSArray arrayWithObject:@"test"]]; + RKLogIntegerAsBinary(tableController.state); + assertThatBool([tableController isLoading], is(equalToBool(NO))); + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatInteger(tableController.state, is(equalToInteger(RKTableControllerStateNormal))); +} + +- (void)testErrorLoadsSetsStateToError { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); + assertThatBool([tableController isLoading], is(equalToBool(NO))); + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + NSError *error = [NSError errorWithDomain:@"Test" code:1234 userInfo:nil]; + [tableController objectLoader:mockLoader didFailWithError:error]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatBool([tableController isError], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); +} + +- (void)testSecondaryLoadAfterErrorSetsStateToErrorAndLoading { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); + assertThatBool([tableController isLoading], is(equalToBool(NO))); + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + NSError *error = [NSError errorWithDomain:@"Test" code:1234 userInfo:nil]; + [tableController objectLoader:mockLoader didFailWithError:error]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatBool([tableController isError], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + [tableController requestDidStartLoad:mockLoader]; + assertThatBool([tableController isLoading], is(equalToBool(YES))); + assertThatBool([tableController isError], is(equalToBool(YES))); +} + +- (void)testEmptyLoadSetsStateToEmpty { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController loadEmpty]; + assertThatBool([tableController isEmpty], is(equalToBool(YES))); +} + +- (void)testSecondaryLoadAfterEmptySetsStateToEmptyAndLoading { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController loadEmpty]; + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + assertThatBool([tableController isLoading], is(equalToBool(NO))); + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + [tableController requestDidStartLoad:mockLoader]; + assertThatBool([tableController isLoading], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); +} + +- (void)testTransitionToOfflineAfterLoadSetsStateToOfflineAndLoaded { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + id mockManager = [OCMockObject partialMockForObject:objectManager]; + BOOL isOnline = YES; + [[[mockManager stub] andReturnValue:OCMOCK_VALUE(isOnline)] isOnline]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = mockManager; + [tableController loadEmpty]; + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + assertThatBool([tableController isLoading], is(equalToBool(NO))); + assertThatBool([tableController isOffline], is(equalToBool(NO))); + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + [tableController requestDidStartLoad:mockLoader]; + assertThatBool([tableController isLoading], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + isOnline = NO; + id mockManager2 = [OCMockObject partialMockForObject:objectManager]; + [[[mockManager2 stub] andReturnValue:OCMOCK_VALUE(isOnline)] isOnline]; + tableController.objectManager = mockManager2; + [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOfflineNotification object:tableController.objectManager]; + assertThatBool(tableController.isOffline, is(equalToBool(YES))); +} + #pragma mark - State Views - (void)testPermitYouToOverlayAnImageOnTheTable { @@ -2019,6 +2222,7 @@ [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController loadEmpty]; // Load to change the isLoaded state UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; tableController.imageForOffline = image; @@ -2096,7 +2300,6 @@ } - (void)testCallTheDelegateBeforeHidingTheSwipeView { -// RKLogConfigureByName("RestKit/UI", RKLogLevelTrace); RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.cellSwipeViewsEnabled = YES; From 680ca666d653b18f44a330f5caba7b2800b9af7f Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 24 May 2012 12:32:25 -0400 Subject: [PATCH 08/12] Update RKTestFactory to silence logging output during setup/tear down operations. closes #764 * Introduced new logging helpers for silencing components. * Check for existence of data store at path before firing deletion to avoid log warning on failure. * Silence logging for reachability and cache during factory initialization of RKClient and RKObjectManager * Adjust log levels on cache components from info to debug --- Code/CoreData/RKEntityByAttributeCache.m | 4 +-- Code/Support/RKLog.h | 8 ++++- Code/Testing/RKTestFactory.m | 39 +++++++++++++++++------- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/Code/CoreData/RKEntityByAttributeCache.m b/Code/CoreData/RKEntityByAttributeCache.m index 4ce93813..4a64204c 100644 --- a/Code/CoreData/RKEntityByAttributeCache.m +++ b/Code/CoreData/RKEntityByAttributeCache.m @@ -99,7 +99,7 @@ - (void)load { - RKLogInfo(@"Loading entity cache for Entity '%@' by attribute '%@'", self.entity.name, self.attribute); + RKLogDebug(@"Loading entity cache for Entity '%@' by attribute '%@'", self.entity.name, self.attribute); NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; [fetchRequest setEntity:self.entity]; [fetchRequest setResultType:NSManagedObjectIDResultType]; @@ -126,7 +126,7 @@ - (void)flush { - RKLogInfo(@"Flushing entity cache for Entity '%@' by attribute '%@'", self.entity.name, self.attribute); + RKLogDebug(@"Flushing entity cache for Entity '%@' by attribute '%@'", self.entity.name, self.attribute); self.attributeValuesToObjectIDs = nil; } diff --git a/Code/Support/RKLog.h b/Code/Support/RKLog.h index 14113986..0bb6b456 100644 --- a/Code/Support/RKLog.h +++ b/Code/Support/RKLog.h @@ -138,6 +138,12 @@ lcl_configure_by_name("App", level); } \ } while(false); +/** + Temporarily turns off logging for the given logging component during execution of the block. + After the block has finished execution, the logging level is restored to its previous state. + */ +#define RKLogSilenceComponentWhileExecutingBlock(component, _block) \ + RKLogToComponentWithLevelWhileExecutingBlock(component, RKLogLevelOff, _block) /** Temporarily changes the logging level for the configured RKLogComponent and executes the block. Any logging @@ -149,7 +155,7 @@ lcl_configure_by_name("App", level); /** - Temporarily turns off logging for the execution of the block. + Temporarily turns off logging for current logging component during execution of the block. After the block has finished execution, the logging level is restored to its previous state. */ #define RKLogSilenceWhileExecutingBlock(_block) \ diff --git a/Code/Testing/RKTestFactory.m b/Code/Testing/RKTestFactory.m index 0c157f8b..426261aa 100644 --- a/Code/Testing/RKTestFactory.m +++ b/Code/Testing/RKTestFactory.m @@ -77,20 +77,32 @@ static RKTestFactory *sharedFactory = nil; - (void)defineDefaultFactories { [self defineFactory:RKTestFactoryDefaultNamesClient withBlock:^id { - RKClient *client = [RKClient clientWithBaseURL:self.baseURL]; - client.requestQueue.suspended = NO; - [client.reachabilityObserver getFlags]; + __block RKClient *client; + + RKLogSilenceComponentWhileExecutingBlock(lcl_cRestKitNetworkReachability, ^{ + RKLogSilenceComponentWhileExecutingBlock(lcl_cRestKitSupport, ^{ + client = [RKClient clientWithBaseURL:self.baseURL]; + client.requestQueue.suspended = NO; + [client.reachabilityObserver getFlags]; + }); + }); return client; }]; [self defineFactory:RKTestFactoryDefaultNamesObjectManager withBlock:^id { - RKObjectManager *objectManager = [RKObjectManager managerWithBaseURL:self.baseURL]; - RKObjectMappingProvider *mappingProvider = [self objectFromFactory:RKTestFactoryDefaultNamesMappingProvider]; - objectManager.mappingProvider = mappingProvider; - - // Force reachability determination - [objectManager.client.reachabilityObserver getFlags]; + __block RKObjectManager *objectManager; + + RKLogSilenceComponentWhileExecutingBlock(lcl_cRestKitNetworkReachability, ^{ + RKLogSilenceComponentWhileExecutingBlock(lcl_cRestKitSupport, ^{ + objectManager = [RKObjectManager managerWithBaseURL:self.baseURL]; + RKObjectMappingProvider *mappingProvider = [self objectFromFactory:RKTestFactoryDefaultNamesMappingProvider]; + objectManager.mappingProvider = mappingProvider; + + // Force reachability determination + [objectManager.client.reachabilityObserver getFlags]; + }); + }); return objectManager; }]; @@ -194,7 +206,12 @@ static RKTestFactory *sharedFactory = nil; { [RKObjectManager setDefaultMappingQueue:dispatch_queue_create("org.restkit.ObjectMapping", DISPATCH_QUEUE_SERIAL)]; [RKObjectMapping setDefaultDateFormatters:nil]; - [RKManagedObjectStore deleteStoreInApplicationDataDirectoryWithFilename:RKTestFactoryDefaultStoreFilename]; + + // Delete the store if it exists + NSString *path = [[RKDirectory applicationDataDirectory] stringByAppendingPathComponent:RKTestFactoryDefaultStoreFilename]; + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + [RKManagedObjectStore deleteStoreInApplicationDataDirectoryWithFilename:RKTestFactoryDefaultStoreFilename]; + } if ([self respondsToSelector:@selector(didSetUp)]) { [self didSetUp]; @@ -218,7 +235,7 @@ static RKTestFactory *sharedFactory = nil; NSString* cachePath = [RKDirectory cachesDirectory]; BOOL success = [[NSFileManager defaultManager] removeItemAtPath:cachePath error:&error]; if (success) { - RKLogInfo(@"Cleared cache directory..."); + RKLogDebug(@"Cleared cache directory..."); success = [[NSFileManager defaultManager] createDirectoryAtPath:cachePath withIntermediateDirectories:YES attributes:nil error:&error]; if (!success) { RKLogError(@"Failed creation of cache path '%@': %@", cachePath, [error localizedDescription]); From 59c449c2e282160b5570b66abd70954aaee9bead Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 24 May 2012 12:43:25 -0400 Subject: [PATCH 09/12] Cleanups to and test coverage expansion of RKTableController API's. refs #765 --- Code/Testing/RKTableControllerTestDelegate.h | 31 ++ Code/Testing/RKTableControllerTestDelegate.m | 139 +++++++ Code/UI/RKAbstractTableController.h | 13 +- Code/UI/RKAbstractTableController.m | 43 +- Code/UI/RKAbstractTableController_Internals.h | 13 +- Code/UI/RKFetchedResultsTableController.h | 12 + Code/UI/RKFetchedResultsTableController.m | 57 ++- Code/UI/RKTableController.h | 8 +- Code/UI/RKTableController.m | 37 +- RestKit.xcodeproj/project.pbxproj | 12 + .../UI/RKFetchedResultsTableControllerTest.m | 373 ++++++++++-------- Tests/Application/UI/RKTableControllerTest.m | 337 ++++++---------- 12 files changed, 607 insertions(+), 468 deletions(-) create mode 100644 Code/Testing/RKTableControllerTestDelegate.h create mode 100644 Code/Testing/RKTableControllerTestDelegate.m diff --git a/Code/Testing/RKTableControllerTestDelegate.h b/Code/Testing/RKTableControllerTestDelegate.h new file mode 100644 index 00000000..ad70ef4d --- /dev/null +++ b/Code/Testing/RKTableControllerTestDelegate.h @@ -0,0 +1,31 @@ +// +// RKTableControllerTestDelegate.h +// RestKit +// +// Created by Blake Watters on 5/23/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#if TARGET_OS_IPHONE +#import "RKTableController.h" +#import "RKFetchedResultsTableController.h" + +@interface RKAbstractTableControllerTestDelegate : NSObject + +@property(nonatomic, readonly, getter = isCancelled) BOOL cancelled; +@property(nonatomic, assign) NSTimeInterval timeout; +@property(nonatomic, assign) BOOL awaitingResponse; + ++ (id)tableControllerDelegate; +- (void)waitForLoad; + +@end + +@interface RKTableControllerTestDelegate : RKAbstractTableControllerTestDelegate +@end + +@interface RKFetchedResultsTableControllerTestDelegate : RKAbstractTableControllerTestDelegate + +@end + +#endif diff --git a/Code/Testing/RKTableControllerTestDelegate.m b/Code/Testing/RKTableControllerTestDelegate.m new file mode 100644 index 00000000..31332e75 --- /dev/null +++ b/Code/Testing/RKTableControllerTestDelegate.m @@ -0,0 +1,139 @@ +// +// RKTableControllerTestDelegate.m +// RestKit +// +// Created by Blake Watters on 5/23/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import "RKTableControllerTestDelegate.h" +#import "RKLog.h" + +#if TARGET_OS_IPHONE + +@implementation RKAbstractTableControllerTestDelegate + +@synthesize timeout = _timeout; +@synthesize awaitingResponse = _awaitingResponse; +@synthesize cancelled = _cancelled; + ++ (id)tableControllerDelegate { + return [[self new] autorelease]; +} + +- (id)init { + self = [super init]; + if (self) { + _timeout = 1.0; + _awaitingResponse = NO; + _cancelled = NO; + } + + return self; +} + +- (void)waitForLoad { + _awaitingResponse = YES; + NSDate *startDate = [NSDate date]; + + while (_awaitingResponse) { + RKLogTrace(@"Awaiting response = %d", _awaitingResponse); + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + if ([[NSDate date] timeIntervalSinceDate:startDate] > self.timeout) { + NSLog(@"%@: Timed out!!!", self); + _awaitingResponse = NO; + [NSException raise:nil format:@"*** Operation timed out after %f seconds...", self.timeout]; + } + } +} + +#pragma RKTableControllerDelegate methods + +- (void)tableControllerDidFinishLoad:(RKAbstractTableController *)tableController { + _awaitingResponse = NO; +} + +- (void)tableController:(RKAbstractTableController*)tableController didFailLoadWithError:(NSError *)error { + _awaitingResponse = NO; +} + +- (void)tableControllerDidCancelLoad:(RKAbstractTableController *)tableController { + _awaitingResponse = NO; + _cancelled = YES; +} + +- (void)tableControllerDidFinalizeLoad:(RKAbstractTableController *)tableController { + _awaitingResponse = NO; +} + +// NOTE - Delegate methods below are implemented to allow trampoline through +// OCMock expectations + +- (void)tableControllerDidStartLoad:(RKAbstractTableController *)tableController +{} + +- (void)tableControllerDidBecomeEmpty:(RKAbstractTableController *)tableController +{} + +- (void)tableController:(RKAbstractTableController *)tableController willLoadTableWithObjectLoader:(RKObjectLoader *)objectLoader +{} + +- (void)tableController:(RKAbstractTableController *)tableController didLoadTableWithObjectLoader:(RKObjectLoader *)objectLoader +{} + +- (void)tableController:(RKAbstractTableController*)tableController willBeginEditing:(id)object atIndexPath:(NSIndexPath*)indexPath +{} + +- (void)tableController:(RKAbstractTableController*)tableController didEndEditing:(id)object atIndexPath:(NSIndexPath*)indexPath +{} + +- (void)tableController:(RKAbstractTableController*)tableController didInsertSection:(RKTableSection*)section atIndex:(NSUInteger)sectionIndex +{} + +- (void)tableController:(RKAbstractTableController*)tableController didRemoveSection:(RKTableSection*)section atIndex:(NSUInteger)sectionIndex +{} + +- (void)tableController:(RKAbstractTableController*)tableController didInsertObject:(id)object atIndexPath:(NSIndexPath*)indexPath +{} + +- (void)tableController:(RKAbstractTableController*)tableController didUpdateObject:(id)object atIndexPath:(NSIndexPath*)indexPath +{} + +- (void)tableController:(RKAbstractTableController*)tableController didDeleteObject:(id)object atIndexPath:(NSIndexPath*)indexPath +{} + +- (void)tableController:(RKAbstractTableController*)tableController willAddSwipeView:(UIView*)swipeView toCell:(UITableViewCell*)cell forObject:(id)object +{} + +- (void)tableController:(RKAbstractTableController*)tableController willRemoveSwipeView:(UIView*)swipeView fromCell:(UITableViewCell*)cell forObject:(id)object +{} + +- (void)tableController:(RKTableController *)tableController didLoadObjects:(NSArray *)objects inSection:(NSUInteger)sectionIndex +{} + +- (void)tableController:(RKAbstractTableController *)tableController willDisplayCell:(UITableViewCell *)cell forObject:(id)object atIndexPath:(NSIndexPath *)indexPath +{} + +- (void)tableController:(RKAbstractTableController *)tableController didSelectCell:(UITableViewCell *)cell forObject:(id)object atIndexPath:(NSIndexPath *)indexPath +{} + +@end + +@implementation RKTableControllerTestDelegate + +- (void)tableController:(RKTableController *)tableController didLoadObjects:(NSArray *)objects inSection:(RKTableSection *)section +{} + +@end + +@implementation RKFetchedResultsTableControllerTestDelegate + +- (void)tableController:(RKFetchedResultsTableController *)tableController didInsertSectionAtIndex:(NSUInteger)sectionIndex +{} + +- (void)tableController:(RKFetchedResultsTableController *)tableController didDeleteSectionAtIndex:(NSUInteger)sectionIndex +{} + +@end + +#endif diff --git a/Code/UI/RKAbstractTableController.h b/Code/UI/RKAbstractTableController.h index 088bf112..d6faea9f 100755 --- a/Code/UI/RKAbstractTableController.h +++ b/Code/UI/RKAbstractTableController.h @@ -152,6 +152,7 @@ typedef NSUInteger RKTableControllerState; */ - (NSIndexPath *)indexPathForObject:(id)object; - (UITableViewCell *)cellForObject:(id)object; +- (void)reloadRowForObject:(id)object withRowAnimation:(UITableViewRowAnimation)rowAnimation; ///----------------------------------------------------------------------------- /// @name Header and Footer Rows @@ -399,7 +400,6 @@ typedef NSUInteger RKTableControllerState; - (void)tableControllerDidFinishLoad:(RKAbstractTableController *)tableController; - (void)tableController:(RKAbstractTableController *)tableController didFailLoadWithError:(NSError *)error; - (void)tableControllerDidCancelLoad:(RKAbstractTableController *)tableController; -- (void)tableController:(RKAbstractTableController *)tableController didLoadObjects:(NSArray *)objects inSection:(NSUInteger)sectionIndex; // NOT IMPLEMENTED /** Sent to the delegate when the controller is really and truly finished loading/updating, whether from the network or from Core Data, @@ -422,11 +422,6 @@ typedef NSUInteger RKTableControllerState; */ - (void)tableControllerDidBecomeOffline:(RKAbstractTableController *)tableController; -// Sections -// TODO: Can these even be implemented??? -- (void)tableController:(RKAbstractTableController *)tableController didInsertSectionAtIndex:(NSUInteger)sectionIndex; -- (void)tableController:(RKAbstractTableController *)tableController didRemoveSectionAtIndex:(NSUInteger)sectionIndex; - // Objects - (void)tableController:(RKAbstractTableController *)tableController didInsertObject:(id)object atIndexPath:(NSIndexPath *)indexPath; - (void)tableController:(RKAbstractTableController *)tableController didUpdateObject:(id)object atIndexPath:(NSIndexPath *)indexPath; @@ -440,16 +435,10 @@ typedef NSUInteger RKTableControllerState; - (void)tableController:(RKAbstractTableController *)tableController willAddSwipeView:(UIView *)swipeView toCell:(UITableViewCell *)cell forObject:(id)object; - (void)tableController:(RKAbstractTableController *)tableController willRemoveSwipeView:(UIView *)swipeView fromCell:(UITableViewCell *)cell forObject:(id)object; -// BELOW NOT YET IMPLEMENTED - // Cells - (void)tableController:(RKAbstractTableController *)tableController willDisplayCell:(UITableViewCell *)cell forObject:(id)object atIndexPath:(NSIndexPath *)indexPath; - (void)tableController:(RKAbstractTableController *)tableController didSelectCell:(UITableViewCell *)cell forObject:(id)object atIndexPath:(NSIndexPath *)indexPath; -// Objects -- (void)tableControllerDidBeginUpdates:(RKAbstractTableController *)tableController; -- (void)tableControllerDidEndUpdates:(RKAbstractTableController *)tableController; - @end #endif // TARGET_OS_IPHONE diff --git a/Code/UI/RKAbstractTableController.m b/Code/UI/RKAbstractTableController.m index fefc8cfd..9351bbfc 100755 --- a/Code/UI/RKAbstractTableController.m +++ b/Code/UI/RKAbstractTableController.m @@ -112,7 +112,7 @@ static NSString * lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey" self = [self init]; if (self) { self.tableView = theTableView; - self.viewController = theViewController; + _viewController = theViewController; // Assign directly to avoid side-effect of overloaded accessor method self.variableHeightRows = NO; self.defaultRowAnimation = UITableViewRowAnimationFade; self.overlayFrame = CGRectZero; @@ -201,8 +201,6 @@ static NSString * lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey" } - (void)setViewController:(UIViewController *)viewController { - _viewController = viewController; - if ([viewController isKindOfClass:[UITableViewController class]]) { self.tableView = [(UITableViewController*)viewController tableView]; } @@ -466,6 +464,10 @@ static NSString * lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey" RKLogTrace(@"%@: Invoking onSelectCellForObjectAtIndexPath block with cellMapping %@ for object %@ at indexPath = %@", self, cell, object, indexPath); cellMapping.onSelectCellForObjectAtIndexPath(cell, object, indexPath); } + + if ([self.delegate respondsToSelector:@selector(tableController:didSelectCell:forObject:atIndexPath:)]) { + [self.delegate tableController:self didSelectCell:cell forObject:object atIndexPath:indexPath]; + } } - (void)tableView:(UITableView *)theTableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { @@ -476,6 +478,10 @@ static NSString * lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey" if (cellMapping.onCellWillAppearForObjectAtIndexPath) { cellMapping.onCellWillAppearForObjectAtIndexPath(cell, mappableObject, indexPath); } + + if ([self.delegate respondsToSelector:@selector(tableController:willDisplayCell:forObject:atIndexPath:)]) { + [self.delegate tableController:self willDisplayCell:cell forObject:mappableObject atIndexPath:indexPath]; + } // Informal protocol // TODO: Needs documentation!!! @@ -633,7 +639,7 @@ static NSString * lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey" - (void)requestDidStartLoad:(RKRequest *)request { RKLogTrace(@"tableController %@ started loading.", self); - self.loading = YES; + [self didStartLoad]; } - (void)requestDidCancelLoad:(RKRequest *)request { @@ -673,8 +679,7 @@ static NSString * lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey" - (void)objectLoader:(RKObjectLoader *)objectLoader didFailWithError:(NSError *)error { RKLogError(@"tableController %@ failed network load with error: %@", self, error); - self.error = error; - [self didFinishLoad]; + [self didFailLoadWithError:error]; } - (void)objectLoaderDidFinishLoading:(RKObjectLoader *)objectLoader { @@ -686,13 +691,26 @@ static NSString * lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey" [self didFinishLoad]; } +- (void)didStartLoad { + self.loading = YES; +} + +- (void)didFailLoadWithError:(NSError *)error { + self.error = error; + [self didFinishLoad]; +} + - (void)didFinishLoad { self.empty = [self isConsideredEmpty]; self.loading = [self.objectLoader isLoading]; // Mutate loading state after we have adjusted empty self.loaded = YES; - - if (self.delegate && [_delegate respondsToSelector:@selector(tableControllerDidFinalizeLoad:)]) { - [_delegate performSelector:@selector(tableControllerDidFinalizeLoad:) withObject:self]; + + if (![self isEmpty] && ![self isLoading]) { + [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadObjectsNotification object:self]; + } + + if (self.delegate && [_delegate respondsToSelector:@selector(tableControllerDidFinalizeLoad:)]) { + [self.delegate performSelector:@selector(tableControllerDidFinalizeLoad:) withObject:self]; } } @@ -1319,4 +1337,11 @@ static NSString * lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey" } } +- (void)reloadRowForObject:(id)object withRowAnimation:(UITableViewRowAnimation)rowAnimation { + NSIndexPath *indexPath = [self indexPathForObject:object]; + if (indexPath) { + [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:rowAnimation]; + } +} + @end diff --git a/Code/UI/RKAbstractTableController_Internals.h b/Code/UI/RKAbstractTableController_Internals.h index ef6a7828..97c7b42b 100644 --- a/Code/UI/RKAbstractTableController_Internals.h +++ b/Code/UI/RKAbstractTableController_Internals.h @@ -38,6 +38,10 @@ @property (nonatomic, readonly) RKCache *cache; @property (nonatomic, retain) UIView *pullToRefreshHeaderView; +#pragma mark - Subclass Load Event Hooks + +- (void)didStartLoad; + /** Must be invoked when the table controller has finished loading. @@ -45,6 +49,7 @@ and cleaning up the table overlay view. */ - (void)didFinishLoad; +- (void)didFailLoadWithError:(NSError *)error; #pragma mark - Table View Overlay @@ -60,14 +65,6 @@ - (void)pullToRefreshStateChanged:(UIGestureRecognizer *)gesture; - (void)resetPullToRefreshRecognizer; -#pragma mark - State Mutators - -- (void)setLoading:(BOOL)loading; -- (void)setLoaded:(BOOL)loaded; -- (void)setEmpty:(BOOL)empty; -- (void)setOffline:(BOOL)offline; -- (void)setErrorState:(BOOL)error; - /** Returns a Boolean value indicating if the table controller should be considered empty and transitioned into the empty state. diff --git a/Code/UI/RKFetchedResultsTableController.h b/Code/UI/RKFetchedResultsTableController.h index adf837d8..127a212f 100755 --- a/Code/UI/RKFetchedResultsTableController.h +++ b/Code/UI/RKFetchedResultsTableController.h @@ -22,6 +22,17 @@ typedef UIView *(^RKFetchedResultsTableViewViewForHeaderInSectionBlock)(NSUInteger sectionIndex, NSString *sectionTitle); +@class RKFetchedResultsTableController; +@protocol RKFetchedResultsTableControllerDelegate + +@optional + +// Sections +- (void)tableController:(RKFetchedResultsTableController *)tableController didInsertSectionAtIndex:(NSUInteger)sectionIndex; +- (void)tableController:(RKFetchedResultsTableController *)tableController didDeleteSectionAtIndex:(NSUInteger)sectionIndex; + +@end + /** Instances of RKFetchedResultsTableController provide an interface for driving a UITableView */ @@ -33,6 +44,7 @@ typedef UIView *(^RKFetchedResultsTableViewViewForHeaderInSectionBlock)(NSUInteg BOOL _isEmptyBeforeAnimation; } +@property (nonatomic, assign) id delegate; @property (nonatomic, retain, readonly) NSFetchedResultsController *fetchedResultsController; @property (nonatomic, copy) NSString *resourcePath; @property (nonatomic, retain) NSFetchRequest *fetchRequest; diff --git a/Code/UI/RKFetchedResultsTableController.m b/Code/UI/RKFetchedResultsTableController.m index 09f5e4fb..ccdd5b31 100755 --- a/Code/UI/RKFetchedResultsTableController.m +++ b/Code/UI/RKFetchedResultsTableController.m @@ -17,6 +17,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // + #import "RKFetchedResultsTableController.h" #import "RKAbstractTableController_Internals.h" #import "RKManagedObjectStore.h" @@ -33,12 +34,13 @@ @interface RKFetchedResultsTableController () @property (nonatomic, retain, readwrite) NSFetchedResultsController *fetchedResultsController; -- (void)performFetch; +- (BOOL)performFetch:(NSError **)error; - (void)updateSortedArray; @end @implementation RKFetchedResultsTableController +@dynamic delegate; @synthesize fetchedResultsController = _fetchedResultsController; @synthesize resourcePath = _resourcePath; @synthesize heightForHeaderInSection = _heightForHeaderInSection; @@ -77,19 +79,31 @@ #pragma mark - Helpers -- (void)performFetch { +- (BOOL)performFetch:(NSError **)error { // TODO: We could be doing a KVO on the predicate/sortDescriptors/sectionKeyPath and intelligently deleting the cache [NSFetchedResultsController deleteCacheWithName:_fetchedResultsController.cacheName]; - - NSError* error; - BOOL success = [_fetchedResultsController performFetch:&error]; + BOOL success = [_fetchedResultsController performFetch:error]; if (!success) { - self.error = error; - RKLogError(@"performFetch failed with error: %@", [error localizedDescription]); + RKLogError(@"performFetch failed with error: %@", [*error localizedDescription]); + return NO; } else { RKLogTrace(@"performFetch completed successfully"); - [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadObjectsNotification object:self]; + for (NSUInteger index = 0; index < [self sectionCount]; index++) { + if ([self.delegate respondsToSelector:@selector(tableController:didInsertSectionAtIndex:)]) { + [self.delegate tableController:self didInsertSectionAtIndex:index]; + } + + if ([self.delegate respondsToSelector:@selector(tableController:didInsertObject:atIndexPath:)]) { + for (NSUInteger row = 0; row < [self numberOfRowsInSection:index]; row++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:index]; + id object = [self objectForRowAtIndexPath:indexPath]; + [self.delegate tableController:self didInsertObject:object atIndexPath:indexPath]; + } + } + } } + + return YES; } - (void)updateSortedArray { @@ -238,10 +252,14 @@ [_fetchedResultsController release]; _fetchedResultsController.delegate = self; - - [self performFetch]; + // Perform the load + NSError *error; + [self didStartLoad]; + BOOL success = [self performFetch:&error]; + if (! success) { + [self didFailLoadWithError:error]; + } [self updateSortedArray]; - [self.tableView reloadData]; [self didFinishLoad]; @@ -512,7 +530,7 @@ - (void)controllerWillChangeContent:(NSFetchedResultsController*)controller { RKLogTrace(@"Beginning updates for fetchedResultsController (%@). Current section count = %d (resource path: %@)", controller, [[controller sections] count], _resourcePath); - if(_sortSelector) return; + if (_sortSelector) return; [self.tableView beginUpdates]; _isEmptyBeforeAnimation = [self isEmpty]; @@ -523,17 +541,25 @@ atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { - if(_sortSelector) return; + if (_sortSelector) return; switch (type) { case NSFetchedResultsChangeInsert: [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; + + if ([self.delegate respondsToSelector:@selector(tableController:didInsertSectionAtIndex:)]) { + [self.delegate tableController:self didInsertSectionAtIndex:sectionIndex]; + } break; case NSFetchedResultsChangeDelete: [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; + + if ([self.delegate respondsToSelector:@selector(tableController:didDeleteSectionAtIndex:)]) { + [self.delegate tableController:self didDeleteSectionAtIndex:sectionIndex]; + } break; default: @@ -548,7 +574,7 @@ forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath { - if(_sortSelector) return; + if (_sortSelector) return; NSIndexPath* adjIndexPath = [self indexPathForFetchedResultsIndexPath:indexPath]; NSIndexPath* adjNewIndexPath = [self indexPathForFetchedResultsIndexPath:newIndexPath]; @@ -595,13 +621,12 @@ [self updateSortedArray]; - if(_sortSelector) { + if (_sortSelector) { [self.tableView reloadData]; } else { [self.tableView endUpdates]; } - [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadObjectsNotification object:self]; [self didFinishLoad]; } diff --git a/Code/UI/RKTableController.h b/Code/UI/RKTableController.h index 87e4f734..9cf8d8ad 100644 --- a/Code/UI/RKTableController.h +++ b/Code/UI/RKTableController.h @@ -33,8 +33,7 @@ @optional -- (void)tableController:(RKAbstractTableController *)tableController didInsertSection:(RKTableSection *)section atIndex:(NSUInteger)sectionIndex; -- (void)tableController:(RKAbstractTableController *)tableController didRemoveSection:(RKTableSection *)section atIndex:(NSUInteger)sectionIndex; +- (void)tableController:(RKTableController *)tableController didLoadObjects:(NSArray *)objects inSection:(RKTableSection *)section; @end @@ -50,9 +49,6 @@ - (void)loadObjects:(NSArray *)objects inSection:(NSUInteger)sectionIndex; - (void)loadEmpty; -// Move to superclass??? -- (void)reloadRowForObject:(id)object withRowAnimation:(UITableViewRowAnimation)rowAnimation; - /** Load an array of RKTableItems into table cells of the specified class. A table cell mapping will be constructed on your behalf and yielded to the block for configuration. @@ -134,7 +130,6 @@ Returns the first section with the specified header title. @param title The header title. */ -// MOVED - (RKTableSection *)sectionWithHeaderTitle:(NSString *)title; /** @@ -143,7 +138,6 @@ @param section Must be a valid non nil RKTableViewSection. @return The index of the given section if contained within the receiver, otherwise NSNotFound. */ -// MOVED - (NSUInteger)indexForSection:(RKTableSection *)section; // Coalesces a series of table view updates performed within the block into diff --git a/Code/UI/RKTableController.m b/Code/UI/RKTableController.m index f7d6d257..580845eb 100644 --- a/Code/UI/RKTableController.m +++ b/Code/UI/RKTableController.m @@ -86,7 +86,7 @@ [self.sections removeObjectsAtIndexes:indexes]; } -- (void)replaceObjectsAtIndexes:(NSIndexSet *)indexes withObjects:(NSArray *)objects { +- (void)replaceSectionsAtIndexes:(NSIndexSet *)indexes withObjects:(NSArray *)objects { [self.sections replaceObjectsAtIndexes:indexes withObjects:objects]; } @@ -98,11 +98,6 @@ } [[self sectionsProxy] addObject:section]; - - // TODO: move into KVO? - if ([self.delegate respondsToSelector:@selector(tableController:didInsertSection:atIndex:)]) { - [self.delegate tableController:self didInsertSection:section atIndex:[self.sections indexOfObject:section]]; - } } - (void)removeSection:(RKTableSection *)section { @@ -112,10 +107,6 @@ reason:@"Tables must always have at least one section" userInfo:nil]; } - NSUInteger index = [self.sections indexOfObject:section]; - if ([self.delegate respondsToSelector:@selector(tableController:didRemoveSection:atIndex:)]) { - [self.delegate tableController:self didRemoveSection:section atIndex:index]; - } [[self sectionsProxy] removeObject:section]; } @@ -123,10 +114,6 @@ NSAssert(section, @"Cannot insert a nil section"); section.tableController = self; [[self sectionsProxy] insertObject:section atIndex:index]; - - if ([self.delegate respondsToSelector:@selector(tableController:didInsertSection:atIndex:)]) { - [self.delegate tableController:self didInsertSection:section atIndex:index]; - } } - (void)removeSectionAtIndex:(NSUInteger)index { @@ -135,21 +122,10 @@ reason:@"Tables must always have at least one section" userInfo:nil]; } - RKTableSection* section = [self.sections objectAtIndex:index]; - if ([self.delegate respondsToSelector:@selector(tableController:didRemoveSection:atIndex:)]) { - [self.delegate tableController:self didRemoveSection:section atIndex:index]; - } [[self sectionsProxy] removeObjectAtIndex:index]; } - (void)removeAllSections:(BOOL)recreateFirstSection { - NSUInteger sectionCount = [self.sections count]; - for (NSUInteger index = 0; index < sectionCount; index++) { - RKTableSection* section = [self.sections objectAtIndex:index]; - if ([self.delegate respondsToSelector:@selector(tableController:didRemoveSection:atIndex:)]) { - [self.delegate tableController:self didRemoveSection:section atIndex:index]; - } - } [[self sectionsProxy] removeAllObjects]; if (recreateFirstSection) { @@ -204,7 +180,9 @@ [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:self.defaultRowAnimation]; - [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadObjectsNotification object:self]; + if ([self.delegate respondsToSelector:@selector(tableController:didLoadObjects:inSection:)]) { + [self.delegate tableController:self didLoadObjects:objects inSection:section]; + } // The load is finalized via network callbacks for // dynamic table controllers @@ -348,13 +326,6 @@ } } -- (void)reloadRowForObject:(id)object withRowAnimation:(UITableViewRowAnimation)rowAnimation { - NSIndexPath *indexPath = [self indexPathForObject:object]; - if (indexPath) { - [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:rowAnimation]; - } -} - - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; if ([keyPath isEqualToString:@"sections"]) { diff --git a/RestKit.xcodeproj/project.pbxproj b/RestKit.xcodeproj/project.pbxproj index 7444f778..9f55bfc2 100644 --- a/RestKit.xcodeproj/project.pbxproj +++ b/RestKit.xcodeproj/project.pbxproj @@ -675,6 +675,10 @@ 25DB7509151BD551009F01AF /* NSManagedObject+ActiveRecordTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25DB7507151BD551009F01AF /* NSManagedObject+ActiveRecordTest.m */; }; 25E36E0215195CED00F9E448 /* RKFetchRequestMappingCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25E36E0115195CED00F9E448 /* RKFetchRequestMappingCacheTest.m */; }; 25E36E0315195CED00F9E448 /* RKFetchRequestMappingCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25E36E0115195CED00F9E448 /* RKFetchRequestMappingCacheTest.m */; }; + 25E4DAB4156DA97F00A5C84B /* RKTableControllerTestDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 25E4DAB2156DA97F00A5C84B /* RKTableControllerTestDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25E4DAB5156DA97F00A5C84B /* RKTableControllerTestDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 25E4DAB2156DA97F00A5C84B /* RKTableControllerTestDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25E4DAB6156DA97F00A5C84B /* RKTableControllerTestDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 25E4DAB3156DA97F00A5C84B /* RKTableControllerTestDelegate.m */; }; + 25E4DAB7156DA97F00A5C84B /* RKTableControllerTestDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 25E4DAB3156DA97F00A5C84B /* RKTableControllerTestDelegate.m */; }; 25EC1A2C14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.h in Headers */ = {isa = PBXBuildFile; fileRef = 25EC1A2A14F6FDAC00C3CF3F /* RKObjectManager+RKTableController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25EC1A2D14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.h in Headers */ = {isa = PBXBuildFile; fileRef = 25EC1A2A14F6FDAC00C3CF3F /* RKObjectManager+RKTableController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25EC1A2E14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.m in Sources */ = {isa = PBXBuildFile; fileRef = 25EC1A2B14F6FDAC00C3CF3F /* RKObjectManager+RKTableController.m */; }; @@ -1173,6 +1177,8 @@ 25CAAA9315254E7800CAE5D7 /* ArrayOfHumans.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ArrayOfHumans.json; sourceTree = ""; }; 25DB7507151BD551009F01AF /* NSManagedObject+ActiveRecordTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObject+ActiveRecordTest.m"; sourceTree = ""; }; 25E36E0115195CED00F9E448 /* RKFetchRequestMappingCacheTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKFetchRequestMappingCacheTest.m; sourceTree = ""; }; + 25E4DAB2156DA97F00A5C84B /* RKTableControllerTestDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKTableControllerTestDelegate.h; path = Testing/RKTableControllerTestDelegate.h; sourceTree = ""; }; + 25E4DAB3156DA97F00A5C84B /* RKTableControllerTestDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKTableControllerTestDelegate.m; path = Testing/RKTableControllerTestDelegate.m; sourceTree = ""; }; 25EC1A2A14F6FDAC00C3CF3F /* RKObjectManager+RKTableController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RKObjectManager+RKTableController.h"; sourceTree = ""; }; 25EC1A2B14F6FDAC00C3CF3F /* RKObjectManager+RKTableController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RKObjectManager+RKTableController.m"; sourceTree = ""; }; 25EC1AB814F8019F00C3CF3F /* RKRefreshGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKRefreshGestureRecognizer.h; sourceTree = ""; }; @@ -1974,6 +1980,8 @@ 25055B8E14EEF40000B9C4DD /* RKMappingTestExpectation.m */, 252EFB2414D9B6F2004863C8 /* Testing.h */, 25C954A415542A47005C9E08 /* RKTestConstants.m */, + 25E4DAB2156DA97F00A5C84B /* RKTableControllerTestDelegate.h */, + 25E4DAB3156DA97F00A5C84B /* RKTableControllerTestDelegate.m */, ); name = Testing; sourceTree = ""; @@ -2264,6 +2272,7 @@ 259D98541550C69A008C90F5 /* RKEntityByAttributeCache.h in Headers */, 259D985E155218E5008C90F5 /* RKEntityCache.h in Headers */, 25545959155F0527007D7625 /* RKBenchmark.h in Headers */, + 25E4DAB4156DA97F00A5C84B /* RKTableControllerTestDelegate.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2388,6 +2397,7 @@ 259D98551550C69A008C90F5 /* RKEntityByAttributeCache.h in Headers */, 259D985F155218E5008C90F5 /* RKEntityCache.h in Headers */, 2554595A155F0527007D7625 /* RKBenchmark.h in Headers */, + 25E4DAB5156DA97F00A5C84B /* RKTableControllerTestDelegate.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2796,6 +2806,7 @@ 259D98561550C69A008C90F5 /* RKEntityByAttributeCache.m in Sources */, 259D9860155218E5008C90F5 /* RKEntityCache.m in Sources */, 2554595B155F0527007D7625 /* RKBenchmark.m in Sources */, + 25E4DAB6156DA97F00A5C84B /* RKTableControllerTestDelegate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2985,6 +2996,7 @@ 259D98571550C69A008C90F5 /* RKEntityByAttributeCache.m in Sources */, 259D9861155218E5008C90F5 /* RKEntityCache.m in Sources */, 2554595C155F0527007D7625 /* RKBenchmark.m in Sources */, + 25E4DAB7156DA97F00A5C84B /* RKTableControllerTestDelegate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Tests/Application/UI/RKFetchedResultsTableControllerTest.m b/Tests/Application/UI/RKFetchedResultsTableControllerTest.m index 220f7903..70430320 100644 --- a/Tests/Application/UI/RKFetchedResultsTableControllerTest.m +++ b/Tests/Application/UI/RKFetchedResultsTableControllerTest.m @@ -14,6 +14,7 @@ #import "RKEvent.h" #import "RKAbstractTableController_Internals.h" #import "RKManagedObjectCaching.h" +#import "RKTableControllerTestDelegate.h" // Expose the object loader delegate for testing purposes... @interface RKFetchedResultsTableController () @@ -31,20 +32,21 @@ @end -@interface RKFetchedResultsTableControllerSpecViewController : UIViewController +@interface RKFetchedResultsTableControllerSpecViewController : UITableViewController @end @implementation RKFetchedResultsTableControllerSpecViewController @end @interface RKFetchedResultsTableControllerTest : RKTestCase - @end @implementation RKFetchedResultsTableControllerTest - (void)setUp { [RKTestFactory setUp]; + + [[[[UIApplication sharedApplication] windows] objectAtIndex:0] setRootViewController:nil]; } - (void)tearDown { @@ -57,7 +59,7 @@ [humanMapping mapKeyPath:@"id" toAttribute:@"railsID"]; [humanMapping mapAttributes:@"name", nil]; humanMapping.primaryKeyAttribute = @"railsID"; - + [RKHuman truncateAll]; assertThatInt([RKHuman count:nil], is(equalToInt(0))); RKHuman* blake = [RKHuman createEntity]; @@ -70,11 +72,11 @@ [store save:&error]; assertThat(error, is(nilValue())); assertThatInt([RKHuman count:nil], is(equalToInt(2))); - + RKObjectManager* objectManager = [RKTestFactory objectManager]; [objectManager.mappingProvider setMapping:humanMapping forKeyPath:@"human"]; objectManager.objectStore = store; - + [objectManager.mappingProvider setObjectMapping:humanMapping forResourcePathPattern:@"/JSON/humans/all\\.json" withFetchRequestBlock:^NSFetchRequest *(NSString *resourcePath) { return [RKHuman requestAllSortedBy:@"name" ascending:YES]; }]; @@ -88,7 +90,7 @@ [eventMapping mapAttributes:@"location", @"summary", nil]; eventMapping.primaryKeyAttribute = @"eventID"; [RKEvent truncateAll]; - + assertThatInt([RKEvent count:nil], is(equalToInt(0))); RKEvent *nakedEvent = [RKEvent createEntity]; nakedEvent.eventID = @"RK4424"; @@ -99,11 +101,11 @@ [store save:&error]; assertThat(error, is(nilValue())); assertThatInt([RKEvent count:nil], is(equalToInt(1))); - + RKObjectManager* objectManager = [RKTestFactory objectManager]; [objectManager.mappingProvider addObjectMapping:eventMapping]; objectManager.objectStore = store; - + id mockMappingProvider = [OCMockObject partialMockForObject:objectManager.mappingProvider]; [[[mockMappingProvider stub] andReturn:[RKEvent requestAllSortedBy:@"eventType" ascending:YES]] fetchRequestForResourcePath:@"/JSON/NakedEvents.json"]; } @@ -114,14 +116,14 @@ [humanMapping mapKeyPath:@"id" toAttribute:@"railsID"]; [humanMapping mapAttributes:@"name", nil]; humanMapping.primaryKeyAttribute = @"railsID"; - + [RKHuman truncateAll]; assertThatInt([RKHuman count:nil], is(equalToInt(0))); - + RKObjectManager* objectManager = [RKTestFactory objectManager]; [objectManager.mappingProvider setMapping:humanMapping forKeyPath:@"human"]; objectManager.objectStore = store; - + id mockMappingProvider = [OCMockObject partialMockForObject:objectManager.mappingProvider]; [[[mockMappingProvider stub] andReturn:[RKHuman requestAllSortedBy:@"name" ascending:YES]] fetchRequestForResourcePath:@"/JSON/humans/all.json"]; [[[mockMappingProvider stub] andReturn:[RKHuman requestAllSortedBy:@"name" ascending:YES]] fetchRequestForResourcePath:@"/empty/array"]; @@ -139,40 +141,34 @@ - (void)testLoadWithATableViewControllerAndResourcePath { [self bootstrapStoreAndCache]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; - + assertThat(tableController.viewController, is(equalTo(viewController))); - assertThat(tableController.tableView, is(equalTo(tableView))); + assertThat(tableController.tableView, is(equalTo(viewController.tableView))); assertThat(tableController.resourcePath, is(equalTo(@"/JSON/humans/all.json"))); } - (void)testLoadWithATableViewControllerAndResourcePathFromNakedObjects { [self bootstrapNakedObjectStoreAndCache]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/NakedEvents.json"; [tableController setObjectMappingForClass:[RKEvent class]]; [tableController loadTable]; - + assertThat(tableController.viewController, is(equalTo(viewController))); - assertThat(tableController.tableView, is(equalTo(tableView))); + assertThat(tableController.tableView, is(equalTo(viewController.tableView))); assertThat(tableController.resourcePath, is(equalTo(@"/JSON/NakedEvents.json"))); - + RKTableViewCellMapping* cellMapping = [RKTableViewCellMapping mappingForClass:[UITableViewCell class]]; [cellMapping mapKeyPath:@"summary" toAttribute:@"textLabel.text"]; RKTableViewCellMappings* mappings = [RKTableViewCellMappings new]; [mappings setCellMapping:cellMapping forClass:[RKEvent class]]; tableController.cellMappings = mappings; - + UITableViewCell* cell = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; assertThat(cell.textLabel.text, is(equalTo(@"Shindig"))); } @@ -180,21 +176,17 @@ - (void)testLoadWithATableViewControllerAndResourcePathAndPredicateAndSortDescriptors { [self bootstrapStoreAndCache]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; NSPredicate* predicate = [NSPredicate predicateWithValue:TRUE]; NSArray* sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.predicate = predicate; tableController.sortDescriptors = sortDescriptors; [tableController loadTable]; - + assertThat(tableController.viewController, is(equalTo(viewController))); - assertThat(tableController.tableView, is(equalTo(tableView))); assertThat(tableController.resourcePath, is(equalTo(@"/JSON/humans/all.json"))); assertThat(tableController.fetchRequest, is(notNilValue())); assertThat([tableController.fetchRequest predicate], is(equalTo(predicate))); @@ -203,18 +195,14 @@ - (void)testLoadWithATableViewControllerAndResourcePathAndSectionNameAndCacheName { [self bootstrapStoreAndCache]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; tableController.cacheName = @"allHumansCache"; [tableController loadTable]; - + assertThat(tableController.viewController, is(equalTo(viewController))); - assertThat(tableController.tableView, is(equalTo(tableView))); assertThat(tableController.resourcePath, is(equalTo(@"/JSON/humans/all.json"))); assertThat(tableController.fetchRequest, is(notNilValue())); assertThat(tableController.fetchedResultsController.sectionNameKeyPath, is(equalTo(@"name"))); @@ -223,23 +211,19 @@ - (void)testLoadWithAllParams { [self bootstrapStoreAndCache]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; NSPredicate* predicate = [NSPredicate predicateWithValue:TRUE]; NSArray* sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.predicate = predicate; tableController.sortDescriptors = sortDescriptors; tableController.sectionNameKeyPath = @"name"; tableController.cacheName = @"allHumansCache"; [tableController loadTable]; - + assertThat(tableController.viewController, is(equalTo(viewController))); - assertThat(tableController.tableView, is(equalTo(tableView))); assertThat(tableController.resourcePath, is(equalTo(@"/JSON/humans/all.json"))); assertThat(tableController.fetchRequest, is(notNilValue())); assertThat([tableController.fetchRequest predicate], is(equalTo(predicate))); @@ -254,10 +238,10 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; - + assertThatInt(tableController.sectionCount, is(equalToInt(1))); } @@ -269,7 +253,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; [tableController loadTable]; @@ -282,7 +266,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; assertThatInt([tableController rowCount], is(equalToInt(2))); @@ -294,7 +278,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -312,7 +296,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Empty"; @@ -330,7 +314,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Empty"; @@ -348,7 +332,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -369,11 +353,8 @@ - (void)testProperlyCountRowsWithHeaderAndEmptyItemsWhenEmptyShowHeaders { [self bootstrapEmptyStoreAndCache]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -394,11 +375,8 @@ - (void)testProperlyCountRowsWithHeaderAndEmptyItemsWhenFull { [self bootstrapStoreAndCache]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -440,11 +418,11 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; [tableController loadTable]; - + assertThatInt([tableController numberOfSectionsInTableView:tableView], is(equalToInt(2))); } @@ -454,10 +432,10 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; - + assertThatInt([tableController tableView:tableView numberOfRowsInSection:0], is(equalToInt(2))); } @@ -467,11 +445,11 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; [tableController loadTable]; - + assertThat([tableController tableView:tableView titleForHeaderInSection:1], is(equalTo(@"other"))); } @@ -481,16 +459,16 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; - + RKTableViewCellMapping* cellMapping = [RKTableViewCellMapping mappingForClass:[UITableViewCell class]]; [cellMapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; RKTableViewCellMappings* mappings = [RKTableViewCellMappings new]; [mappings setCellMapping:cellMapping forClass:[RKHuman class]]; tableController.cellMappings = mappings; - + UITableViewCell* cell = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; assertThat(cell.textLabel.text, is(equalTo(@"blake"))); } @@ -503,13 +481,14 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; - + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; assertThatBool(blake == [tableController objectForRowAtIndexPath:indexPath], is(equalToBool(YES))); + [tableController release]; } #pragma mark - Editing @@ -520,33 +499,33 @@ [[RKObjectManager sharedManager].router routeClass:[RKHuman class] toResourcePath:@"/humans/:railsID" forMethod:RKRequestMethodDELETE]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.canEditRows = YES; + RKTableViewCellMapping *cellMapping = [RKTableViewCellMapping cellMapping]; + [cellMapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [tableController mapObjectsWithClass:[RKHuman class] toTableCellsWithMapping:cellMapping]; [tableController loadTable]; - + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:1 inSection:0]; NSIndexPath* deleteIndexPath = [NSIndexPath indexPathForRow:0 inSection:0]; RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; - + assertThatInt([tableController rowCount], is(equalToInt(2))); assertThat([tableController objectForRowAtIndexPath:indexPath], is(equalTo(other))); assertThat([tableController objectForRowAtIndexPath:deleteIndexPath], is(equalTo(blake))); BOOL delegateCanEdit = [tableController tableView:tableController.tableView - canEditRowAtIndexPath:deleteIndexPath]; + canEditRowAtIndexPath:deleteIndexPath]; assertThatBool(delegateCanEdit, is(equalToBool(YES))); - + [RKTestNotificationObserver waitForNotificationWithName:RKRequestDidLoadResponseNotification usingBlock:^{ [tableController tableView:tableController.tableView - commitEditingStyle:UITableViewCellEditingStyleDelete - forRowAtIndexPath:deleteIndexPath]; + commitEditingStyle:UITableViewCellEditingStyleDelete + forRowAtIndexPath:deleteIndexPath]; }]; - + assertThatInt([tableController rowCount], is(equalToInt(1))); assertThat([tableController objectForRowAtIndexPath:deleteIndexPath], is(equalTo(other))); assertThatBool([blake isDeleted], is(equalToBool(YES))); @@ -555,36 +534,36 @@ - (void)testLocallyCommitADeleteWhenTheCanEditRowsPropertyIsSet { [self bootstrapStoreAndCache]; [self stubObjectManagerToOnline]; - + UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.canEditRows = YES; [tableController loadTable]; - + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; NSIndexPath* deleteIndexPath = [NSIndexPath indexPathForRow:1 inSection:0]; RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; blake.railsID = nil; other.railsID = nil; - + NSError* error = nil; [blake.managedObjectContext save:&error]; assertThat(error, is(nilValue())); - + assertThatInt([tableController rowCount], is(equalToInt(2))); assertThat([tableController objectForRowAtIndexPath:indexPath], is(equalTo(blake))); assertThat([tableController objectForRowAtIndexPath:deleteIndexPath], is(equalTo(other))); BOOL delegateCanEdit = [tableController tableView:tableController.tableView - canEditRowAtIndexPath:deleteIndexPath]; + canEditRowAtIndexPath:deleteIndexPath]; assertThatBool(delegateCanEdit, is(equalToBool(YES))); [tableController tableView:tableController.tableView - commitEditingStyle:UITableViewCellEditingStyleDelete - forRowAtIndexPath:deleteIndexPath]; + commitEditingStyle:UITableViewCellEditingStyleDelete + forRowAtIndexPath:deleteIndexPath]; assertThatInt([tableController rowCount], is(equalToInt(1))); assertThat([tableController objectForRowAtIndexPath:indexPath], is(equalTo(blake))); } @@ -592,26 +571,26 @@ - (void)testNotCommitADeletionWhenTheCanEditRowsPropertyIsNotSet { [self bootstrapStoreAndCache]; [self stubObjectManagerToOnline]; - + UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; - + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; - + assertThatInt([tableController rowCount], is(equalToInt(2))); BOOL delegateCanEdit = [tableController tableView:tableController.tableView - canEditRowAtIndexPath:indexPath]; + canEditRowAtIndexPath:indexPath]; assertThatBool(delegateCanEdit, is(equalToBool(NO))); [tableController tableView:tableController.tableView - commitEditingStyle:UITableViewCellEditingStyleDelete - forRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + commitEditingStyle:UITableViewCellEditingStyleDelete + forRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; assertThatInt([tableController rowCount], is(equalToInt(2))); assertThat([tableController objectForRowAtIndexPath:indexPath], is(equalTo(blake))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], @@ -621,27 +600,27 @@ - (void)testDoNothingToCommitAnInsertionWhenTheCanEditRowsPropertyIsSet { [self bootstrapStoreAndCache]; [self stubObjectManagerToOnline]; - + UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.canEditRows = YES; [tableController loadTable]; - + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; - + assertThatInt([tableController rowCount], is(equalToInt(2))); BOOL delegateCanEdit = [tableController tableView:tableController.tableView - canEditRowAtIndexPath:indexPath]; + canEditRowAtIndexPath:indexPath]; assertThatBool(delegateCanEdit, is(equalToBool(YES))); [tableController tableView:tableController.tableView - commitEditingStyle:UITableViewCellEditingStyleInsert - forRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + commitEditingStyle:UITableViewCellEditingStyleInsert + forRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; assertThatInt([tableController rowCount], is(equalToInt(2))); assertThat([tableController objectForRowAtIndexPath:indexPath], is(equalTo(blake))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], @@ -654,22 +633,22 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.canMoveRows = YES; [tableController loadTable]; - + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; - + assertThatInt([tableController rowCount], is(equalToInt(2))); BOOL delegateCanMove = [tableController tableView:tableController.tableView - canMoveRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + canMoveRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; assertThatBool(delegateCanMove, is(equalToBool(YES))); [tableController tableView:tableController.tableView - moveRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] - toIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + moveRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] + toIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; assertThatInt([tableController rowCount], is(equalToInt(2))); assertThat([tableController objectForRowAtIndexPath:indexPath], is(equalTo(blake))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], @@ -684,7 +663,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; assertThatBool([tableController isHeaderSection:0], is(equalToBool(YES))); @@ -698,7 +677,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -718,7 +697,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; @@ -738,7 +717,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { @@ -759,7 +738,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; @@ -779,7 +758,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; assertThatBool([tableController isEmptySection:0], is(equalToBool(YES))); @@ -793,7 +772,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; assertThatBool([tableController isEmptyRow:0], is(equalToBool(YES))); @@ -807,7 +786,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -830,7 +809,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; @@ -853,7 +832,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Empty"; @@ -876,7 +855,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -897,7 +876,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; @@ -917,7 +896,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { @@ -939,7 +918,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Empty"; @@ -960,7 +939,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -987,7 +966,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { @@ -1016,7 +995,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1050,7 +1029,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { @@ -1084,7 +1063,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1110,7 +1089,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; RKTableItem* headerRow = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1122,10 +1101,10 @@ tableController.showsHeaderRowsWhenEmpty = NO; tableController.showsFooterRowsWhenEmpty = NO; [tableController loadTable]; - + RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; - + assertThatInt([tableController rowCount], is(equalToInt(3))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalTo(headerRow))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo(blake))); @@ -1139,7 +1118,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; RKTableItem* footerRow = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; @@ -1151,10 +1130,10 @@ tableController.showsHeaderRowsWhenEmpty = NO; tableController.showsFooterRowsWhenEmpty = NO; [tableController loadTable]; - + RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; - + assertThatInt([tableController rowCount], is(equalToInt(3))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalTo(blake))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo(other))); @@ -1168,7 +1147,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1179,7 +1158,7 @@ tableController.showsHeaderRowsWhenEmpty = NO; tableController.showsFooterRowsWhenEmpty = NO; [tableController loadTable]; - + assertThatBool(tableController.isLoaded, is(equalToBool(YES))); assertThatInt([tableController rowCount], is(equalToInt(0))); assertThatBool(tableController.isEmpty, is(equalToBool(YES))); @@ -1191,7 +1170,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; @@ -1202,7 +1181,7 @@ tableController.showsHeaderRowsWhenEmpty = NO; tableController.showsFooterRowsWhenEmpty = NO; [tableController loadTable]; - + assertThatBool(tableController.isLoaded, is(equalToBool(YES))); assertThatInt([tableController rowCount], is(equalToInt(0))); assertThatBool(tableController.isEmpty, is(equalToBool(YES))); @@ -1214,7 +1193,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1237,7 +1216,7 @@ tableController.showsHeaderRowsWhenEmpty = NO; tableController.showsFooterRowsWhenEmpty = NO; [tableController loadTable]; - + assertThatBool(tableController.isLoaded, is(equalToBool(YES))); assertThatInt([tableController rowCount], is(equalToInt(1))); assertThatBool(tableController.isEmpty, is(equalToBool(YES))); @@ -1249,9 +1228,9 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; - + RKTableItem* headerRow = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { @@ -1259,7 +1238,7 @@ }]; }]; [tableController addHeaderRowForItem:headerRow]; - + RKTableItem* footerRow = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { @@ -1267,7 +1246,7 @@ }]; }]; [tableController addFooterRowForItem:footerRow]; - + RKTableItem* emptyItem = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Empty"; tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { @@ -1278,10 +1257,10 @@ tableController.showsHeaderRowsWhenEmpty = NO; tableController.showsFooterRowsWhenEmpty = NO; [tableController loadTable]; - + RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; - + assertThatInt([tableController rowCount], is(equalToInt(4))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalTo(headerRow))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo(blake))); @@ -1296,7 +1275,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1319,7 +1298,7 @@ tableController.showsHeaderRowsWhenEmpty = NO; tableController.showsFooterRowsWhenEmpty = NO; [tableController loadTable]; - + assertThatBool(tableController.isLoaded, is(equalToBool(YES))); assertThatInt([tableController rowCount], is(equalToInt(1))); assertThatBool(tableController.isEmpty, is(equalToBool(YES))); @@ -1331,7 +1310,7 @@ RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1354,7 +1333,7 @@ tableController.showsHeaderRowsWhenEmpty = YES; tableController.showsFooterRowsWhenEmpty = YES; [tableController loadTable]; - + assertThatBool(tableController.isLoaded, is(equalToBool(YES))); assertThatInt([tableController rowCount], is(equalToInt(3))); assertThatBool(tableController.isEmpty, is(equalToBool(YES))); @@ -1363,20 +1342,20 @@ - (void)testShowTheEmptyImageAfterLoadingAnEmptyCollectionIntoAnEmptyFetch { [self bootstrapEmptyStoreAndCache]; [self stubObjectManagerToOnline]; - + UITableView* tableView = [UITableView new]; - + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; - + viewController:viewController]; + UIImage *image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; - + tableController.imageForEmpty = image; tableController.resourcePath = @"/empty/array"; tableController.autoRefreshFromNetwork = YES; [tableController.cache invalidateAll]; - + [RKTestNotificationObserver waitForNotificationWithName:RKTableControllerDidFinishLoadNotification usingBlock:^{ [tableController loadTable]; }]; @@ -1403,4 +1382,86 @@ [observerMock verify]; } +#pragma mark - Delegate Methods + +- (void)testDelegateIsInformedOnInsertSection { + [self bootstrapStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:viewController.tableView viewController:viewController]; + RKTableViewCellMapping *cellMapping = [RKTableViewCellMapping cellMapping]; + [cellMapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [tableController mapObjectsWithClass:[RKHuman class] toTableCellsWithMapping:cellMapping]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.cacheName = @"allHumansCache"; + + RKFetchedResultsTableControllerTestDelegate *delegate = [RKFetchedResultsTableControllerTestDelegate tableControllerDelegate]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[mockDelegate expect] tableController:tableController didInsertSectionAtIndex:0]; + tableController.delegate = mockDelegate; + [[[[UIApplication sharedApplication] windows] objectAtIndex:0] setRootViewController:viewController]; + [tableController loadTable]; + assertThatInt([tableController rowCount], is(equalToInt(2))); + assertThatInt([tableController sectionCount], is(equalToInt(1))); + [mockDelegate verify]; +} + +- (void)testDelegateIsInformedOfDidStartLoad { + [self bootstrapStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:viewController.tableView viewController:viewController]; + RKTableViewCellMapping *cellMapping = [RKTableViewCellMapping cellMapping]; + [cellMapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [tableController mapObjectsWithClass:[RKHuman class] toTableCellsWithMapping:cellMapping]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.cacheName = @"allHumansCache"; + + id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKFetchedResultsTableControllerDelegate)]; + [[mockDelegate expect] tableControllerDidStartLoad:tableController]; + tableController.delegate = mockDelegate; + [[[[UIApplication sharedApplication] windows] objectAtIndex:0] setRootViewController:viewController]; + [tableController loadTable]; + [mockDelegate verify]; +} + +- (void)testDelegateIsInformedOfDidFinishLoad { + [self bootstrapStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:viewController.tableView viewController:viewController]; + RKTableViewCellMapping *cellMapping = [RKTableViewCellMapping cellMapping]; + [cellMapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [tableController mapObjectsWithClass:[RKHuman class] toTableCellsWithMapping:cellMapping]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.cacheName = @"allHumansCache"; + + id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKFetchedResultsTableControllerDelegate)]; + [[mockDelegate expect] tableControllerDidFinishLoad:tableController]; + tableController.delegate = mockDelegate; + [[[[UIApplication sharedApplication] windows] objectAtIndex:0] setRootViewController:viewController]; + [tableController loadTable]; + [mockDelegate verify]; +} + +- (void)testDelegateIsInformedOfDidInsertObjectAtIndexPath { + [self bootstrapStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:viewController.tableView viewController:viewController]; + RKTableViewCellMapping *cellMapping = [RKTableViewCellMapping cellMapping]; + [cellMapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [tableController mapObjectsWithClass:[RKHuman class] toTableCellsWithMapping:cellMapping]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.cacheName = @"allHumansCache"; + + id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKFetchedResultsTableControllerDelegate)]; + [[mockDelegate expect] tableController:tableController didInsertObject:OCMOCK_ANY atIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [[mockDelegate expect] tableController:tableController didInsertObject:OCMOCK_ANY atIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + tableController.delegate = mockDelegate; + [[[[UIApplication sharedApplication] windows] objectAtIndex:0] setRootViewController:viewController]; + [tableController loadTable]; + [mockDelegate verify]; +} + @end diff --git a/Tests/Application/UI/RKTableControllerTest.m b/Tests/Application/UI/RKTableControllerTest.m index 6e21c5cb..aaa33600 100644 --- a/Tests/Application/UI/RKTableControllerTest.m +++ b/Tests/Application/UI/RKTableControllerTest.m @@ -12,103 +12,23 @@ #import "RKTestUser.h" #import "RKMappableObject.h" #import "RKAbstractTableController_Internals.h" +#import "RKTableControllerTestDelegate.h" // Expose the object loader delegate for testing purposes... @interface RKTableController () - (void)animationDidStopAddingSwipeView:(NSString*)animationID finished:(NSNumber*)finished context:(void*)context; @end -@interface RKTestTableControllerDelegate : NSObject - -@property(nonatomic, readonly, getter = isCancelled) BOOL cancelled; -@property(nonatomic, assign) NSTimeInterval timeout; -@property(nonatomic, assign) BOOL awaitingResponse; - -+ (id)tableControllerDelegate; -- (void)waitForLoad; -@end - -@implementation RKTestTableControllerDelegate - -@synthesize timeout = _timeout; -@synthesize awaitingResponse = _awaitingResponse; -@synthesize cancelled = _cancelled; - -+ (id)tableControllerDelegate { - return [[self new] autorelease]; -} - -- (id)init { - self = [super init]; - if (self) { - _timeout = 3; - _awaitingResponse = NO; - _cancelled = NO; - } - - return self; -} - -- (void)waitForLoad { - _awaitingResponse = YES; - NSDate* startDate = [NSDate date]; - - while (_awaitingResponse) { - NSLog(@"Awaiting response = %d", _awaitingResponse); - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - if ([[NSDate date] timeIntervalSinceDate:startDate] > self.timeout) { - NSLog(@"%@: Timed out!!!", self); - _awaitingResponse = NO; - [NSException raise:nil format:@"*** Operation timed out after %f seconds...", self.timeout]; - } - } -} - -#pragma RKTableControllerDelegate methods - -- (void)tableControllerDidFinishLoad:(RKAbstractTableController*)tableController { - _awaitingResponse = NO; -} - -- (void)tableController:(RKAbstractTableController*)tableController didFailLoadWithError:(NSError *)error { - _awaitingResponse = NO; -} - -- (void)tableControllerDidCancelLoad:(RKAbstractTableController*)tableController { - _awaitingResponse = NO; - _cancelled = YES; -} - -// NOTE - Delegate methods below are implemented to allow trampoline through -// OCMock expectations - -- (void)tableControllerDidStartLoad:(RKAbstractTableController*)tableController {} -- (void)tableControllerDidBecomeEmpty:(RKAbstractTableController*)tableController {} -- (void)tableController:(RKAbstractTableController*)tableController willLoadTableWithObjectLoader:(RKObjectLoader*)objectLoader {} -- (void)tableController:(RKAbstractTableController*)tableController didLoadTableWithObjectLoader:(RKObjectLoader*)objectLoader {} -- (void)tableController:(RKAbstractTableController*)tableController willBeginEditing:(id)object atIndexPath:(NSIndexPath*)indexPath {} -- (void)tableController:(RKAbstractTableController*)tableController didEndEditing:(id)object atIndexPath:(NSIndexPath*)indexPath {} -- (void)tableController:(RKAbstractTableController*)tableController didInsertSection:(RKTableSection*)section atIndex:(NSUInteger)sectionIndex {} -- (void)tableController:(RKAbstractTableController*)tableController didRemoveSection:(RKTableSection*)section atIndex:(NSUInteger)sectionIndex {} -- (void)tableController:(RKAbstractTableController*)tableController didInsertObject:(id)object atIndexPath:(NSIndexPath*)indexPath {} -- (void)tableController:(RKAbstractTableController*)tableController didUpdateObject:(id)object atIndexPath:(NSIndexPath*)indexPath {} -- (void)tableController:(RKAbstractTableController*)tableController didDeleteObject:(id)object atIndexPath:(NSIndexPath*)indexPath {} -- (void)tableController:(RKAbstractTableController*)tableController willAddSwipeView:(UIView*)swipeView toCell:(UITableViewCell*)cell forObject:(id)object {} -- (void)tableController:(RKAbstractTableController*)tableController willRemoveSwipeView:(UIView*)swipeView fromCell:(UITableViewCell*)cell forObject:(id)object {} -- (void)tableControllerDidFinalizeLoad:(RKAbstractTableController *)tableController {} - -@end - @interface RKTableControllerTestTableViewController : UITableViewController @end @implementation RKTableControllerTestTableViewController @end -@interface RKTableControllerSpecViewController : UIViewController +@interface RKTableControllerTestViewController : UIViewController @end -@implementation RKTableControllerSpecViewController +@implementation RKTableControllerTestViewController @end @interface RKTestUserTableViewCell : UITableViewCell @@ -145,7 +65,7 @@ - (void)testInitializeWithATableViewAndViewController { UITableView* tableView = [UITableView new]; - RKTableControllerSpecViewController* viewController = [RKTableControllerSpecViewController new]; + RKTableControllerTestViewController* viewController = [RKTableControllerTestViewController new]; RKTableController* tableController = [RKTableController tableControllerWithTableView:tableView forViewController:viewController]; assertThat(tableController.viewController, is(equalTo(viewController))); assertThat(tableController.tableView, is(equalTo(tableView))); @@ -192,153 +112,84 @@ RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - tableController.delegate = mockDelegate; [tableController addSection:section]; assertThatInt([tableController.sections count], is(equalToInt(2))); - STAssertNoThrow([mockDelegate verify], nil); } - (void)testConnectTheSectionToTheTableModelOnAdd { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - tableController.delegate = mockDelegate; [tableController addSection:section]; assertThat(section.tableController, is(equalTo(tableController))); - STAssertNoThrow([mockDelegate verify], nil); } - (void)testConnectTheSectionToTheCellMappingsOfTheTableModelWhenNil { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - tableController.delegate = mockDelegate; assertThat(section.cellMappings, is(nilValue())); [tableController addSection:section]; assertThat(section.cellMappings, is(equalTo(tableController.cellMappings))); - STAssertNoThrow([mockDelegate verify], nil); } - (void)testNotConnectTheSectionToTheCellMappingsOfTheTableModelWhenNonNil { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - tableController.delegate = mockDelegate; section.cellMappings = [NSMutableDictionary dictionary]; [tableController addSection:section]; assertThatBool(section.cellMappings == tableController.cellMappings, is(equalToBool(NO))); - STAssertNoThrow([mockDelegate verify], nil); } - (void)testCountTheSections { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - tableController.delegate = mockDelegate; [tableController addSection:section]; assertThatInt(tableController.sectionCount, is(equalToInt(2))); - STAssertNoThrow([mockDelegate verify], nil); } - (void)testRemoveASection { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didRemoveSection:section - atIndex:1]; - tableController.delegate = mockDelegate; [tableController addSection:section]; assertThatInt(tableController.sectionCount, is(equalToInt(2))); [tableController removeSection:section]; assertThatInt(tableController.sectionCount, is(equalToInt(1))); - STAssertNoThrow([mockDelegate verify], nil); } - (void)testNotLetRemoveTheLastSection { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - tableController.delegate = mockDelegate; [tableController addSection:section]; assertThatInt(tableController.sectionCount, is(equalToInt(2))); [tableController removeSection:section]; - STAssertNoThrow([mockDelegate verify], nil); } -- (void)testInsertASectionAtASpecificIndex { +- (void)testInsertASectionAtATestificIndex { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* referenceSection = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:referenceSection - atIndex:2]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; - tableController.delegate = mockDelegate; [tableController insertSection:referenceSection atIndex:2]; assertThatInt(tableController.sectionCount, is(equalToInt(6))); assertThat([tableController.sections objectAtIndex:2], is(equalTo(referenceSection))); - STAssertNoThrow([mockDelegate verify], nil); } - (void)testRemoveASectionByIndex { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didRemoveSection:section - atIndex:1]; - tableController.delegate = mockDelegate; [tableController addSection:section]; assertThatInt(tableController.sectionCount, is(equalToInt(2))); [tableController removeSectionAtIndex:1]; assertThatInt(tableController.sectionCount, is(equalToInt(1))); - STAssertNoThrow([mockDelegate verify], nil); } - (void)testRaiseAnExceptionWhenAttemptingToRemoveTheLastSection { @@ -360,58 +211,18 @@ RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* referenceSection = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:referenceSection - atIndex:2]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; - tableController.delegate = mockDelegate; [tableController insertSection:referenceSection atIndex:2]; assertThatInt(tableController.sectionCount, is(equalToInt(6))); assertThat([tableController sectionAtIndex:2], is(equalTo(referenceSection))); - STAssertNoThrow([mockDelegate verify], nil); } - (void)testRemoveAllSections { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:OCMOCK_ANY - atIndex:0]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:OCMOCK_ANY - atIndex:1]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:OCMOCK_ANY - atIndex:2]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:OCMOCK_ANY - atIndex:3]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:OCMOCK_ANY - atIndex:4]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didRemoveSection:OCMOCK_ANY - atIndex:0]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didRemoveSection:OCMOCK_ANY - atIndex:1]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didRemoveSection:OCMOCK_ANY - atIndex:2]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didRemoveSection:OCMOCK_ANY - atIndex:3]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didRemoveSection:OCMOCK_ANY - atIndex:4]; - tableController.delegate = mockDelegate; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; @@ -419,7 +230,6 @@ assertThatInt(tableController.sectionCount, is(equalToInt(5))); [tableController removeAllSections]; assertThatInt(tableController.sectionCount, is(equalToInt(1))); - STAssertNoThrow([mockDelegate verify], nil); } - (void)testReturnASectionByHeaderTitle { @@ -435,7 +245,7 @@ } - (void)testNotifyTheTableViewOnSectionInsertion { - RKTableControllerSpecViewController *viewController = [RKTableControllerSpecViewController new]; + RKTableControllerTestViewController *viewController = [RKTableControllerTestViewController new]; id mockTableView = [OCMockObject niceMockForClass:[UITableView class]]; RKTableController *tableController = [RKTableController tableControllerWithTableView:mockTableView forViewController:viewController]; [[mockTableView expect] insertSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:tableController.defaultRowAnimation]; @@ -444,7 +254,7 @@ } - (void)testNotifyTheTableViewOnSectionRemoval { - RKTableControllerSpecViewController *viewController = [RKTableControllerSpecViewController new]; + RKTableControllerTestViewController *viewController = [RKTableControllerTestViewController new]; id mockTableView = [OCMockObject niceMockForClass:[UITableView class]]; RKTableController *tableController = [RKTableController tableControllerWithTableView:mockTableView forViewController:viewController]; [[mockTableView expect] insertSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:tableController.defaultRowAnimation]; @@ -456,7 +266,7 @@ } - (void)testNotifyTheTableOfSectionRemovalAndReaddWhenRemovingAllSections { - RKTableControllerSpecViewController *viewController = [RKTableControllerSpecViewController new]; + RKTableControllerTestViewController *viewController = [RKTableControllerTestViewController new]; id mockTableView = [OCMockObject niceMockForClass:[UITableView class]]; RKTableController *tableController = [RKTableController tableControllerWithTableView:mockTableView forViewController:viewController]; [[mockTableView expect] deleteSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:tableController.defaultRowAnimation]; @@ -468,7 +278,7 @@ [mockTableView verify]; } -#pragma mark - UITableViewDataSource specs +#pragma mark - UITableViewDataSource Tests - (void)testRaiseAnExceptionIfSentAMessageWithATableViewItIsNotBoundTo { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; @@ -649,7 +459,7 @@ assertThat([tableController sectionAtIndex:0].objects, is(equalTo(objects))); } -- (void)testLoadAnArrayOfObjectsToTheSpecifiedSection { +- (void)testLoadAnArrayOfObjectsToTheTestifiedSection { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; [tableController addSection:[RKTableSection section]]; @@ -677,10 +487,8 @@ - (void)testAllowYouToTriggerAnEmptyLoad { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; - RKLogIntegerAsBinary(tableController.state); assertThatBool([tableController isLoaded], is(equalToBool(NO))); [tableController loadEmpty]; - RKLogIntegerAsBinary(tableController.state); assertThatBool([tableController isLoaded], is(equalToBool(YES))); assertThatBool([tableController isEmpty], is(equalToBool(YES))); } @@ -698,7 +506,7 @@ [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate tableControllerDelegate]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate tableControllerDelegate]; delegate.timeout = 10; tableController.delegate = delegate; [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { @@ -884,7 +692,7 @@ [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; tableController.sectionNameKeyPath = @"name"; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate tableControllerDelegate]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate tableControllerDelegate]; delegate.timeout = 10; tableController.delegate = delegate; [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { @@ -898,7 +706,7 @@ assertThatInt(tableController.rowCount, is(equalToInt(3))); } -#pragma mark - RKTableViewDelegate specs +#pragma mark - RKTableViewDelegate Tests - (void)testNotifyTheDelegateWhenLoadingStarts { RKObjectManager* objectManager = [RKTestFactory objectManager]; @@ -910,7 +718,7 @@ [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - id mockDelegate = [OCMockObject partialMockForObject:[RKTestTableControllerDelegate new]]; + id mockDelegate = [OCMockObject partialMockForObject:[RKTableControllerTestDelegate new]]; [[[mockDelegate expect] andForwardToRealObject] tableControllerDidStartLoad:tableController]; tableController.delegate = mockDelegate; [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { @@ -932,7 +740,7 @@ [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableControllerDidFinishLoad:tableController]; tableController.delegate = mockDelegate; @@ -955,7 +763,7 @@ [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[mockDelegate expect] tableControllerDidFinalizeLoad:tableController]; tableController.delegate = mockDelegate; @@ -978,7 +786,7 @@ [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController didFailLoadWithError:OCMOCK_ANY]; tableController.delegate = mockDelegate; @@ -1002,7 +810,7 @@ [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; delegate.timeout = 5; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableControllerDidBecomeEmpty:tableController]; @@ -1026,7 +834,7 @@ [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController willLoadTableWithObjectLoader:OCMOCK_ANY]; tableController.delegate = mockDelegate; @@ -1049,7 +857,7 @@ [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController didLoadTableWithObjectLoader:OCMOCK_ANY]; tableController.delegate = mockDelegate; @@ -1072,7 +880,7 @@ [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableControllerDidCancelLoad:tableController]; tableController.delegate = mockDelegate; @@ -1089,7 +897,7 @@ RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableItem* tableItem = [RKTableItem tableItem]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController didEndEditing:OCMOCK_ANY @@ -1104,7 +912,7 @@ RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableItem* tableItem = [RKTableItem tableItem]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController willBeginEditing:OCMOCK_ANY @@ -1119,7 +927,7 @@ NSArray* objects = [NSArray arrayWithObject:@"first object"]; RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController didInsertObject:@"first object" @@ -1141,7 +949,7 @@ NSArray* objects = [NSArray arrayWithObjects:@"first object", @"second object", nil]; RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController didInsertObject:@"first object" @@ -1165,7 +973,7 @@ NSArray* objects = [NSArray arrayWithObjects:@"first object", @"second object", nil]; RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController didInsertObject:@"first object" @@ -1185,6 +993,82 @@ STAssertNoThrow([mockDelegate verify], nil); } +- (void)testNotifyTheDelegateWhenObjectsAreLoadedInASection { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = objectManager; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { + mapping.cellClass = [RKTestUserTableViewCell class]; + [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; + }]]; + + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[mockDelegate expect] tableController:tableController didLoadObjects:OCMOCK_ANY inSection:OCMOCK_ANY]; + tableController.delegate = mockDelegate; + + [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [mockDelegate waitForLoad]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testDelegateIsNotifiedOfWillDisplayCellForObjectAtIndexPath { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = objectManager; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { + mapping.cellClass = [RKTestUserTableViewCell class]; + [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + }]]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController willLoadTableWithObjectLoader:OCMOCK_ANY]; + tableController.delegate = mockDelegate; + [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [[mockDelegate expect] tableController:tableController willDisplayCell:OCMOCK_ANY forObject:OCMOCK_ANY atIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [[mockDelegate expect] tableController:tableController willDisplayCell:OCMOCK_ANY forObject:OCMOCK_ANY atIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + [[mockDelegate expect] tableController:tableController willDisplayCell:OCMOCK_ANY forObject:OCMOCK_ANY atIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + [[[UIApplication sharedApplication].windows objectAtIndex:0] setRootViewController:viewController]; + [mockDelegate waitForLoad]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testDelegateIsNotifiedOfDidSelectRowForObjectAtIndexPath { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = objectManager; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { + mapping.cellClass = [RKTestUserTableViewCell class]; + [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + }]]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController willLoadTableWithObjectLoader:OCMOCK_ANY]; + tableController.delegate = mockDelegate; + [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [[mockDelegate expect] tableController:tableController didSelectCell:OCMOCK_ANY forObject:OCMOCK_ANY atIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [[[UIApplication sharedApplication].windows objectAtIndex:0] setRootViewController:viewController]; + [mockDelegate waitForLoad]; + [tableController tableView:tableController.tableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + STAssertNoThrow([mockDelegate verify], nil); +} + #pragma mark - Notifications - (void)testPostANotificationWhenLoadingStarts { @@ -1200,7 +1084,7 @@ id observerMock = [OCMockObject observerMock]; [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidStartLoadNotification object:tableController]; [[observerMock expect] notificationWithName:RKTableControllerDidStartLoadNotification object:tableController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; tableController.delegate = delegate; [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { @@ -1225,7 +1109,7 @@ id observerMock = [OCMockObject observerMock]; [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidFinishLoadNotification object:tableController]; [[observerMock expect] notificationWithName:RKTableControllerDidFinishLoadNotification object:tableController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; tableController.delegate = delegate; [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { @@ -1251,7 +1135,7 @@ id observerMock = [OCMockObject observerMock]; [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidLoadObjectsNotification object:tableController]; [[observerMock expect] notificationWithName:RKTableControllerDidLoadObjectsNotification object:tableController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; tableController.delegate = delegate; [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { @@ -1277,7 +1161,7 @@ id observerMock = [OCMockObject observerMock]; [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidLoadErrorNotification object:tableController]; [[observerMock expect] notificationWithName:RKTableControllerDidLoadErrorNotification object:tableController userInfo:OCMOCK_ANY]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; tableController.delegate = delegate; [tableController loadTableFromResourcePath:@"/fail" usingBlock:^(RKObjectLoader* objectLoader) { @@ -1304,7 +1188,7 @@ id observerMock = [OCMockObject observerMock]; [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidLoadEmptyNotification object:tableController]; [[observerMock expect] notificationWithName:RKTableControllerDidLoadEmptyNotification object:tableController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; tableController.delegate = delegate; [tableController loadTableFromResourcePath:@"/empty/array" usingBlock:^(RKObjectLoader* objectLoader) { objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { @@ -1341,7 +1225,6 @@ assertThatBool([tableController isLoading], is(equalToBool(NO))); id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; [tableController objectLoader:mockLoader didLoadObjects:[NSArray arrayWithObject:@"test"]]; - RKLogIntegerAsBinary(tableController.state); assertThatBool([tableController isLoading], is(equalToBool(NO))); assertThatBool([tableController isLoaded], is(equalToBool(YES))); assertThatInteger(tableController.state, is(equalToInteger(RKTableControllerStateNormal))); @@ -1785,7 +1668,7 @@ assertThatBool(cellThree.hidden, is(equalToBool(NO))); } -#pragma mark - UITableViewDelegate specs +#pragma mark - UITableViewDelegate Tests - (void)testInvokeTheOnSelectCellForObjectAtIndexPathBlockHandler { RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; @@ -2285,7 +2168,7 @@ RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.cellSwipeViewsEnabled = YES; RKTableItem* tableItem = [RKTableItem tableItem]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController willAddSwipeView:OCMOCK_ANY @@ -2304,7 +2187,7 @@ RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.cellSwipeViewsEnabled = YES; RKTableItem* tableItem = [RKTableItem tableItem]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController willAddSwipeView:OCMOCK_ANY From 1dc765c774aacbae3e3f14210016927d357eb6dc Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 24 May 2012 19:06:04 -0400 Subject: [PATCH 10/12] Fix static analyzer warning --- Code/UI/RKFetchedResultsTableController.m | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Code/UI/RKFetchedResultsTableController.m b/Code/UI/RKFetchedResultsTableController.m index ccdd5b31..c564f99d 100755 --- a/Code/UI/RKFetchedResultsTableController.m +++ b/Code/UI/RKFetchedResultsTableController.m @@ -234,8 +234,7 @@ } else { fetchRequest = _fetchRequest; } - NSAssert(fetchRequest != nil, @"Attempted to load RKFetchedResultsTableController with nil fetchRequest for resourcePath %@, fetchRequest %@", - _resourcePath, _fetchRequest); + NSAssert(fetchRequest != nil, @"Attempted to load RKFetchedResultsTableController with nil fetchRequest for resourcePath %@, fetchRequest %@", _resourcePath, _fetchRequest); if (_predicate) { [fetchRequest setPredicate:_predicate]; @@ -244,12 +243,10 @@ [fetchRequest setSortDescriptors:_sortDescriptors]; } - self.fetchedResultsController = - [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest - managedObjectContext:[NSManagedObjectContext contextForCurrentThread] - sectionNameKeyPath:_sectionNameKeyPath - cacheName:_cacheName]; - [_fetchedResultsController release]; + _fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest + managedObjectContext:[NSManagedObjectContext contextForCurrentThread] + sectionNameKeyPath:_sectionNameKeyPath + cacheName:_cacheName]; _fetchedResultsController.delegate = self; // Perform the load From fa88dd44ef4911b7a21af5c87c5fd0ae7dadc7e6 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 24 May 2012 19:09:39 -0400 Subject: [PATCH 11/12] Fix type mismatch assertion in cases where JSON/XML payload contains a string value for an integer primary key. fixes #754 --- .../NSEntityDescription+RKAdditions.h | 9 +++++++ .../NSEntityDescription+RKAdditions.m | 15 ++++++++--- Code/CoreData/RKManagedObjectMapping.m | 3 ++- .../CoreData/RKManagedObjectMappingTest.m | 26 +++++++++++++++++++ 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/Code/CoreData/NSEntityDescription+RKAdditions.h b/Code/CoreData/NSEntityDescription+RKAdditions.h index 79cb8a16..5da928b4 100644 --- a/Code/CoreData/NSEntityDescription+RKAdditions.h +++ b/Code/CoreData/NSEntityDescription+RKAdditions.h @@ -81,4 +81,13 @@ extern NSString * const RKEntityDescriptionPrimaryKeyAttributeValuePredicateSubs */ - (NSPredicate *)predicateForPrimaryKeyAttributeWithValue:(id)value; +/** + Coerces the given value into the class representing the primary key. Currently support NSString + and NSNumber coercsions. + + @bug **NOTE** This API is temporary and will be deprecated and replaced. + @since 0.10.1 + */ +- (id)coerceValueForPrimaryKey:(id)primaryKeyValue; + @end diff --git a/Code/CoreData/NSEntityDescription+RKAdditions.m b/Code/CoreData/NSEntityDescription+RKAdditions.m index 225bc4aa..c244f4ae 100644 --- a/Code/CoreData/NSEntityDescription+RKAdditions.m +++ b/Code/CoreData/NSEntityDescription+RKAdditions.m @@ -74,12 +74,12 @@ static char primaryKeyAttributeNameKey, primaryKeyPredicateKey; return (NSPredicate *) objc_getAssociatedObject(self, &primaryKeyPredicateKey); } -- (NSPredicate *)predicateForPrimaryKeyAttributeWithValue:(id)value +- (id)coerceValueForPrimaryKey:(id)primaryKeyValue { - id searchValue = value; + id searchValue = primaryKeyValue; Class theClass = [self primaryKeyAttributeClass]; if (theClass) { - // TODO: This coercsion behave should be pluggable and reused from the mapper + // TODO: This coercsion behavior should be pluggable and reused from the mapper if ([theClass isSubclassOfClass:[NSNumber class]] && ![searchValue isKindOfClass:[NSNumber class]]) { // Handle NSString -> NSNumber if ([searchValue isKindOfClass:[NSString class]]) { @@ -92,7 +92,14 @@ static char primaryKeyAttributeNameKey, primaryKeyPredicateKey; } } } - NSDictionary *variables = [NSDictionary dictionaryWithObject:searchValue + + return searchValue; +} + +- (NSPredicate *)predicateForPrimaryKeyAttributeWithValue:(id)value +{ + id substitutionValue = [self coerceValueForPrimaryKey:value]; + NSDictionary *variables = [NSDictionary dictionaryWithObject:substitutionValue forKey:RKEntityDescriptionPrimaryKeyAttributeValuePredicateSubstitutionVariable]; return [[self predicateForPrimaryKeyAttribute] predicateWithSubstitutionVariables:variables]; } diff --git a/Code/CoreData/RKManagedObjectMapping.m b/Code/CoreData/RKManagedObjectMapping.m index 58e6b01d..8008218f 100644 --- a/Code/CoreData/RKManagedObjectMapping.m +++ b/Code/CoreData/RKManagedObjectMapping.m @@ -182,7 +182,8 @@ object = [[[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:[_objectStore managedObjectContextForCurrentThread]] autorelease]; if (primaryKeyAttribute && primaryKeyValue && ![primaryKeyValue isEqual:[NSNull null]]) { - [object setValue:primaryKeyValue forKey:primaryKeyAttribute]; + id coercedPrimaryKeyValue = [entity coerceValueForPrimaryKey:primaryKeyValue]; + [object setValue:coercedPrimaryKeyValue forKey:primaryKeyAttribute]; } if ([self.objectStore.cacheStrategy respondsToSelector:@selector(didCreateObject:)]) { diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingTest.m index 25b45c24..4e05176b 100644 --- a/Tests/Logic/CoreData/RKManagedObjectMappingTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectMappingTest.m @@ -317,4 +317,30 @@ assertThat(cachedObject, is(equalTo(human))); } +- (void)testThatCreationOfNewObjectWithIncorrectTypeValueForPrimaryKeyAddsToCache +{ + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; + store.cacheStrategy = [RKInMemoryManagedObjectCache new]; + [RKHuman truncateAll]; + RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; + mapping.primaryKeyAttribute = @"railsID"; + [RKHuman entity].primaryKeyAttributeName = @"railsID"; + [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"monkey.name" toKeyPath:@"name"]]; + [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"monkey.railsID" toKeyPath:@"railsID"]]; + + [RKHuman truncateAll]; + RKHuman* human = [RKHuman object]; + human.name = @"Testing"; + human.railsID = [NSNumber numberWithInteger:12345]; + [store save:nil]; + assertThatBool([RKHuman hasAtLeastOneEntity], is(equalToBool(YES))); + + NSDictionary* data = [NSDictionary dictionaryWithObject:@"12345" forKey:@"railsID"]; + NSDictionary* nestedDictionary = [NSDictionary dictionaryWithObject:data forKey:@"monkey"]; + RKHuman *object = [mapping mappableObjectForData:nestedDictionary]; + assertThat(object, isNot(nilValue())); + assertThat(object, is(equalTo(human))); + assertThatInteger([object.railsID integerValue], is(equalToInteger(12345))); +} + @end From 8647fa5f428b3a1f25ac809ddd3a4c876d73c333 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Fri, 25 May 2012 13:45:30 -0400 Subject: [PATCH 12/12] Fix issue where grouping objects by section in RKTableController and loading an empty collection would fail to refresh table view. --- Code/UI/RKTableController.m | 3 ++ Tests/Application/UI/RKTableControllerTest.m | 37 ++++++++++++++++++++ Tests/Server/server.rb | 6 ++++ 3 files changed, 46 insertions(+) diff --git a/Code/UI/RKTableController.m b/Code/UI/RKTableController.m index 580845eb..eef6248f 100644 --- a/Code/UI/RKTableController.m +++ b/Code/UI/RKTableController.m @@ -314,6 +314,9 @@ if (self.sectionNameKeyPath) { NSArray *sectionedObjects = [objects sectionsGroupedByKeyPath:self.sectionNameKeyPath]; + if ([sectionedObjects count] == 0) { + [self removeAllSections]; + } for (NSArray *sectionOfObjects in sectionedObjects) { NSUInteger sectionIndex = [sectionedObjects indexOfObject:sectionOfObjects]; if (sectionIndex >= [self sectionCount]) { diff --git a/Tests/Application/UI/RKTableControllerTest.m b/Tests/Application/UI/RKTableControllerTest.m index aaa33600..0378a792 100644 --- a/Tests/Application/UI/RKTableControllerTest.m +++ b/Tests/Application/UI/RKTableControllerTest.m @@ -706,6 +706,43 @@ assertThatInt(tableController.rowCount, is(equalToInt(3))); } +- (void)testLoadingACollectionOfObjectsIntoSectionsAndThenLoadingAnEmptyCollectionChangesTableToEmpty { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + objectManager.client.cachePolicy = RKRequestCachePolicyNone; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = objectManager; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { + mapping.cellClass = [RKTestUserTableViewCell class]; + [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; + }]]; + tableController.sectionNameKeyPath = @"name"; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate tableControllerDelegate]; + delegate.timeout = 10; + tableController.delegate = delegate; + [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [delegate waitForLoad]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatInt(tableController.sectionCount, is(equalToInt(3))); + assertThatInt(tableController.rowCount, is(equalToInt(3))); + delegate = [RKTableControllerTestDelegate tableControllerDelegate]; + delegate.timeout = 10; + tableController.delegate = delegate; + [tableController loadTableFromResourcePath:@"/204" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [delegate waitForLoad]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); +} + #pragma mark - RKTableViewDelegate Tests - (void)testNotifyTheDelegateWhenLoadingStarts { diff --git a/Tests/Server/server.rb b/Tests/Server/server.rb index 96ec0550..3169a844 100644 --- a/Tests/Server/server.rb +++ b/Tests/Server/server.rb @@ -99,6 +99,12 @@ class RestKitTestServer < Sinatra::Base content_type 'application/json' "".to_json end + + get '/204' do + status 204 + content_type 'application/json' + "".to_json + end get '/403' do status 403