Merge branch 'release/0.10.1' into development

This commit is contained in:
Blake Watters
2012-05-25 23:33:54 -04:00
30 changed files with 1919 additions and 994 deletions

View File

@@ -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,9 +71,23 @@ 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;
/**
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

View File

@@ -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,24 +60,46 @@ 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);
}
- (id)coerceValueForPrimaryKey:(id)primaryKeyValue
{
id searchValue = primaryKeyValue;
Class theClass = [self primaryKeyAttributeClass];
if (theClass) {
// 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]]) {
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];
}
}
}
return searchValue;
}
- (NSPredicate *)predicateForPrimaryKeyAttributeWithValue:(id)value
{
NSDictionary *variables = [NSDictionary dictionaryWithObject:value
id substitutionValue = [self coerceValueForPrimaryKey:value];
NSDictionary *variables = [NSDictionary dictionaryWithObject:substitutionValue
forKey:RKEntityDescriptionPrimaryKeyAttributeValuePredicateSubstitutionVariable];
return [[self predicateForPrimaryKeyAttribute] predicateWithSubstitutionVariables:variables];
}

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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:)]) {
@@ -202,17 +203,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;
}
}
}

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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;

View File

@@ -123,7 +123,7 @@
- (void)finalizeLoad:(BOOL)successful {
self.loading = NO;
self.loaded = successful;
if ([self.delegate respondsToSelector:@selector(objectLoaderDidFinishLoading:)]) {
[(NSObject<RKObjectLoaderDelegate>*)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];
}

View File

@@ -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) \
@@ -205,3 +211,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);

View File

@@ -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);
}

View File

@@ -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 <RKAbstractTableControllerDelegate>
@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 <RKTableControllerDelegate>
@end
@interface RKFetchedResultsTableControllerTestDelegate : RKAbstractTableControllerTestDelegate <RKFetchedResultsTableControllerDelegate>
@end
#endif

View File

@@ -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

View File

@@ -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]);

View File

@@ -21,60 +21,102 @@
#if TARGET_OS_IPHONE
#import <UIKit/UIKit.h>
#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 <UITableViewDataSource, UITableViewDelegate> {
@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 <UITableViewDataSource, UITableViewDelegate>
///-----------------------------------------------------------------------------
/// @name Configuring the Table Controller
/////////////////////////////////////////////////////////////////////////
///-----------------------------------------------------------------------------
@property (nonatomic, assign) id<RKTableControllerDelegate> delegate;
@property (nonatomic, readonly) UIViewController* viewController;
@property (nonatomic, readonly) UITableView* tableView;
@property (nonatomic, readonly) NSMutableArray* sections;
@property (nonatomic, assign) id<RKAbstractTableControllerDelegate> delegate;
@property (nonatomic, readonly) UIViewController *viewController;
@property (nonatomic, readonly) UITableView *tableView;
@property (nonatomic, assign) UITableViewRowAnimation defaultRowAnimation;
@property (nonatomic, assign) BOOL pullToRefreshEnabled;
@@ -82,38 +124,39 @@ 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
*/
- (NSIndexPath *)indexPathForObject:(id)object;
- (UITableViewCell *)cellForObject:(id)object;
- (void)reloadRowForObject:(id)object withRowAnimation:(UITableViewRowAnimation)rowAnimation;
/////////////////////////////////////////////////////////////////////////
///-----------------------------------------------------------------------------
/// @name Header and Footer Rows
/////////////////////////////////////////////////////////////////////////
///-----------------------------------------------------------------------------
- (void)addHeaderRowForItem:(RKTableItem *)tableItem;
- (void)addFooterRowForItem:(RKTableItem *)tableItem;
@@ -122,12 +165,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 +187,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 +290,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 +315,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 +333,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,46 +383,45 @@ extern NSString* const RKTableControllerDidBecomeOffline;
@end
@protocol RKTableControllerDelegate <NSObject>
@protocol RKAbstractTableControllerDelegate <NSObject>
@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;
/** 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;
// Objects
- (void)tableController:(RKAbstractTableController *)tableController didInsertObject:(id)object atIndexPath:(NSIndexPath *)indexPath;
- (void)tableController:(RKAbstractTableController *)tableController didUpdateObject:(id)object atIndexPath:(NSIndexPath *)indexPath;
@@ -320,19 +432,13 @@ 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;
// BELOW NOT YET IMPLEMENTED
- (void)tableController:(RKAbstractTableController *)tableController willAddSwipeView:(UIView *)swipeView toCell:(UITableViewCell *)cell forObject:(id)object;
- (void)tableController:(RKAbstractTableController *)tableController willRemoveSwipeView:(UIView *)swipeView fromCell:(UITableViewCell *)cell forObject:(id)object;
// Cells
- (void)tableController:(RKAbstractTableController *)tableController willDisplayCell:(UITableViewCell *)cell forObject:(id)object atIndexPath:(NSIndexPath *)indexPath;
- (void)tableController:(RKAbstractTableController *)tableController didSelectCell:(UITableViewCell *)cell forObject:(id)object atIndexPath:(NSIndexPath *)indexPath;
// Objects
- (void)tableControllerDidBeginUpdates:(RKAbstractTableController *)tableController;
- (void)tableControllerDidEndUpdates:(RKAbstractTableController *)tableController;
@end
#endif // TARGET_OS_IPHONE

File diff suppressed because it is too large Load Diff

View File

@@ -18,25 +18,29 @@
// limitations under the License.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "RKRefreshGestureRecognizer.h"
/*
A private continuation class for subclass implementations of RKAbstractTableController
*/
@interface RKAbstractTableController () <RKObjectLoaderDelegate, RKRefreshTriggerProtocol>
@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;
#pragma mark - Subclass Load Event Hooks
- (void)didStartLoad;
/**
Must be invoked when the table controller has finished loading.
@@ -45,7 +49,7 @@
and cleaning up the table overlay view.
*/
- (void)didFinishLoad;
- (void)updateOfflineImageForOnlineState:(BOOL)isOnline;
- (void)didFailLoadWithError:(NSError *)error;
#pragma mark - Table View Overlay
@@ -61,5 +65,14 @@
- (void)pullToRefreshStateChanged:(UIGestureRecognizer *)gesture;
- (void)resetPullToRefreshRecognizer;
/**
Returns a Boolean value indicating if the table controller
should be considered empty and transitioned into the empty state.
Used by the abstract table controller to trigger state transitions.
**NOTE**: This is an abstract method that MUST be implemented with
a subclass.
*/
- (BOOL)isConsideredEmpty;
@end

View File

@@ -22,6 +22,17 @@
typedef UIView *(^RKFetchedResultsTableViewViewForHeaderInSectionBlock)(NSUInteger sectionIndex, NSString *sectionTitle);
@class RKFetchedResultsTableController;
@protocol RKFetchedResultsTableControllerDelegate <RKAbstractTableControllerDelegate>
@optional
// Sections
- (void)tableController:(RKFetchedResultsTableController *)tableController didInsertSectionAtIndex:(NSUInteger)sectionIndex;
- (void)tableController:(RKFetchedResultsTableController *)tableController didDeleteSectionAtIndex:(NSUInteger)sectionIndex;
@end
/**
Instances of RKFetchedResultsTableController provide an interface for driving a UITableView
*/
@@ -33,7 +44,8 @@ typedef UIView *(^RKFetchedResultsTableViewViewForHeaderInSectionBlock)(NSUInteg
BOOL _isEmptyBeforeAnimation;
}
@property (nonatomic, readonly) NSFetchedResultsController *fetchedResultsController;
@property (nonatomic, assign) id<RKFetchedResultsTableControllerDelegate> delegate;
@property (nonatomic, retain, readonly) NSFetchedResultsController *fetchedResultsController;
@property (nonatomic, copy) NSString *resourcePath;
@property (nonatomic, retain) NSFetchRequest *fetchRequest;
@property (nonatomic, assign) CGFloat heightForHeaderInSection;

View File

@@ -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"
@@ -31,12 +32,15 @@
#define RKLogComponent lcl_cRestKitUI
@interface RKFetchedResultsTableController ()
- (void)performFetch;
@property (nonatomic, retain, readwrite) NSFetchedResultsController *fetchedResultsController;
- (BOOL)performFetch:(NSError **)error;
- (void)updateSortedArray;
@end
@implementation RKFetchedResultsTableController
@dynamic delegate;
@synthesize fetchedResultsController = _fetchedResultsController;
@synthesize resourcePath = _resourcePath;
@synthesize heightForHeaderInSection = _heightForHeaderInSection;
@@ -75,18 +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");
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 {
@@ -217,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];
@@ -226,21 +242,21 @@
if (_sortDescriptors) {
[fetchRequest setSortDescriptors:_sortDescriptors];
}
[_fetchedResultsController setDelegate:nil];
[_fetchedResultsController release];
_fetchedResultsController = nil;
_fetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:[NSManagedObjectContext contextForCurrentThread]
sectionNameKeyPath:_sectionNameKeyPath
cacheName:_cacheName];
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:[NSManagedObjectContext contextForCurrentThread]
sectionNameKeyPath:_sectionNameKeyPath
cacheName:_cacheName];
_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];
@@ -470,7 +486,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 +515,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);
@@ -511,7 +527,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];
@@ -522,17 +538,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:
@@ -547,7 +571,7 @@
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
if(_sortSelector) return;
if (_sortSelector) return;
NSIndexPath* adjIndexPath = [self indexPathForFetchedResultsIndexPath:indexPath];
NSIndexPath* adjNewIndexPath = [self indexPathForFetchedResultsIndexPath:newIndexPath];
@@ -594,13 +618,27 @@
[self updateSortedArray];
if(_sortSelector) {
if (_sortSelector) {
[self.tableView reloadData];
} else {
[self.tableView endUpdates];
}
[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

View File

@@ -29,19 +29,26 @@
#import "RKObjectMapping.h"
#import "RKObjectLoader.h"
@protocol RKTableControllerDelegate <RKAbstractTableControllerDelegate>
@optional
- (void)tableController:(RKTableController *)tableController didLoadObjects:(NSArray *)objects inSection:(RKTableSection *)section;
@end
@interface RKTableController : RKAbstractTableController
/////////////////////////////////////////////////////////////////////////
@property (nonatomic, assign) id<RKTableControllerDelegate> delegate;
///-----------------------------------------------------------------------------
/// @name Static Tables
/////////////////////////////////////////////////////////////////////////
///-----------------------------------------------------------------------------
- (void)loadObjects:(NSArray *)objects;
- (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.
@@ -78,12 +85,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 +109,37 @@
*/
- (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.
*/
- (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.
*/
- (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?

View File

@@ -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];
}
@@ -77,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];
}
@@ -89,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 {
@@ -103,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];
}
@@ -114,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 {
@@ -126,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) {
@@ -194,7 +179,11 @@
}
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:self.defaultRowAnimation];
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
if (nil == self.objectLoader) {
@@ -325,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]) {
@@ -337,13 +329,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"]) {
@@ -376,4 +361,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