I am working on a ground-up rebuild for an app that has a relatively simple purpose. Users can view/favorite entities that are backed by a RESTful API. I own both the client code as well as the API. For this rebuild, I wanted to refactor the networking code out of my view controllers and hopefully move my codebase toward being more understandable by yet to be hired other developers. A few assumptions/goals I had going in.
- View Controllers should neither know about, nor care where/how models are getting their data.
- Models should be able to update/build themselves.
- Any client used to talk to an API should be relatively "pluggable". I use a subclassed
AFHTTPSessionManager
from AFNetworking now, but who knows if that is the case a year from now. - Singleton API clients with delegate callbacks have caused race condition issues for me in the past.
- I'd like to unit test as much of this as possible.
I think I have achieved most of my goals. However, being a relative noob, there is surely room for improvement around either architecture or implementation. Much love in advance for any wisdom. On to it.
Protocols
GTCommunicator.h
#import <Foundation/Foundation.h>
//Class Forward declaration
@class GTBaseEntity;
//Protocol forward declaration. Needed as there is a circular reference
@protocol GTAPICommunicatorDelegate;
@protocol GTAPICommunicator <NSObject>
@required
@property (weak) id<GTAPICommunicatorDelegate> delegate;
- (void)fetchOne:(GTBaseEntity *)entity;
- (void)fetchMany:(NSArray *)arrayOfEntities;
@end
GTAPICommunicatorDelegate.h
#import <Foundation/Foundation.h>
//Protocol forward declaration. Needed as there is a circular reference
@protocol GTAPICommunicator;
@protocol GTAPICommunicatorDelegate <NSObject>
@optional
- (void)client:(id<GTAPICommunicator>)client didFetchRawEntityDictionary:(NSDictionary *)entityDictionary;
- (void)client:(id<GTAPICommunicator>)client didFetchRawEntities:(NSArray *)rawEntityArray;
@required
- (void)client:(id<GTAPICommunicator>)client didFailWithError:(NSError *)error;
@end
GTAPIBackedBaseEntity.h
#import <Foundation/Foundation.h>
//Forward Class Declaration
@class GTBaseEntity;
@protocol GTAPIBackedBaseEntity <NSObject>
@required
@property (nonatomic, strong) GTAPIClient *client;
-(void)fetch:(void (^)(NSError *errorOrNil, GTBaseEntity *returnedEntity))completionBlock;
@end
API Client
GTAPIClient.h
#import "AFHTTPSessionManager.h"
//Protocol import
#import "GTAPICommunicator.h"
@interface GTAPIClient : AFHTTPSessionManager <GTAPICommunicator>
- (instancetype)initWithBaseURL:(NSURL *)url;
@end
GTAPIClient.m
#import "GTAPIClient.h"
//Models
#import "GTBaseEntity.h"
//Protocols
#import "GTAPICommunicatorDelegate.h"
@implementation GTAPIClient
@synthesize delegate;
- (instancetype)initWithBaseURL:(NSURL *)url
{
self = [super initWithBaseURL:url];
if (self) {
self.responseSerializer = [AFJSONResponseSerializer serializer];
self.requestSerializer = [AFHTTPRequestSerializer serializer];
[self.requestSerializer setValue:kGTAPIKey forHTTPHeaderField:@"X-GT-API-Key"];
[self.requestSerializer setValue:[self realUser] forHTTPHeaderField:@"X-GT-Real-User"];
NSLog(@"API User: %@", [self realUser]);
}
return self;
}
#pragma mark - Entity Fetching
- (void)fetchOne:(GTBaseEntity *)entity
{
//Check to make sure the entity passed in is valid and exists
if (!entity || ![self isValidEntity:entity]) {
[NSException raise:NSInternalInconsistencyException format:@"The object passed into fetchOne is not a GTBaseEntity"];
}
//Construct the path
NSString *path = [NSString stringWithFormat:@"%@/%@", entity.type, entity._id];
//Call the API
[self GET:path
parameters:nil
success:^(NSURLSessionDataTask *task, id responseObject) {
//All good, send the raw responseObject back to the delegate. Might want to actually check to make sure this is an NSDictionary
[self.delegate client:self didFetchRawEntityDictionary:responseObject];
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
//Things went badly, send the error back to the delegate
[self.delegate client:self didFailWithError:error];
}];
}
Model
GTBaseEntity.h
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSInteger, GTEntityType) {
GTEntityTypeProject,
GTEntityTypePerson,
GTEntityTypeAgency,
GTEntityTypeOffice,
GTEntityTypeVendor,
GTEntityTypeCategory,
GTEntityTypeProtest,
GTEntityTypeIDV,
GTEntityTypeActivity,
GTEntityTypeUnknown
};
#import "GTAPIClient.h"
//Protocols
#import "GTAPICommunicatorDelegate.h"
#import "GTAPIBackedBaseEntity.h"
@interface GTBaseEntity : NSObject <GTAPICommunicatorDelegate, GTAPIBackedBaseEntity>
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *_id;
@property (nonatomic, strong) NSString *type;
@property (nonatomic, strong) NSDate *timestamp;
@property (nonatomic, readonly) GTEntityType entityType;
@property (nonatomic, readonly) UIImage *icon;
@property (nonatomic, readonly) UIImage *selectedIcon;
@property (nonatomic, readonly) NSDictionary *NTI;
@property (nonatomic, strong) NSMutableDictionary *undefinedProperties;
@property (copy) void (^completionBlock)(NSError *errorOrNil, GTBaseEntity *returnedEntity);
@property (nonatomic, strong) GTAPIClient<GTAPICommunicator> *client;
//Returns a subclass of GTBaseEntity, dependent on the passed in dictionary
+ (instancetype)entityFromDictionary:(NSDictionary *)dictionary;
@end
GTBaseEntity.m
#import "GTBaseEntity.h"
//Subclasses
#import "GTProjectEntity.h"
#import "GTPersonEntity.h"
#import "GTAgencyEntity.h"
#import "GTOfficeEntity.h"
#import "GTVendorEntity.h"
#import "GTCategoryEntity.h"
#import "GTIDVEntity.h"
#import "GTProtestEntity.h"
#import "GTActivityEntity.h"
//API Clients
#import "GTAPIClient.h"
@implementation GTBaseEntity
//Some methods removed for ease of reading
#pragma mark - Fetching
-(void)fetch:(void (^)(NSError *errorOrNil, GTBaseEntity *returnedEntity))completionBlock
{
//Set the passsed in completionBlock for use later on the delegate callbacks
self.completionBlock = completionBlock;
//Get an API Client, set the delegate to self, and tell the client to fetch
self.client.delegate = self;
[self.client fetchOne:self];
}
#pragma mark - Overridden Getters
- (GTAPIClient *)client
{
if (_client) return _client;
else {
//Instantiate new client with url string constant
NSURL *apiURL = [NSURL URLWithString:kGTAPIURLString];
_client = [[GTAPIClient alloc] initWithBaseURL:apiURL];
return _client;
}
}
#pragma mark - GTAPICommunicatorDelegate Methods
- (void)client:(id<GTAPICommunicator>)client didFetchRawEntityDictionary:(NSDictionary *)entityDictionary
{
//Update self with the new values in rawDictionary
[self setValuesForKeysWithDictionary:entityDictionary];
//Fire completionBlock with reference to self
if (self.completionBlock) {
self.completionBlock(nil,self);
self.completionBlock = nil;
}
//Fire notification for other things that may care about this entity being updated
NSNotification *entityFetchedNotification = [NSNotification notificationWithName:kGTEntityFetchedNotification object:self];
[[NSNotificationCenter defaultCenter] postNotification:entityFetchedNotification];
}
- (void)client:(id<GTAPICommunicator>)client didFailWithError:(NSError *)error
{
if (error.code == 404) {
//Try to load from cache if available. If so, fire self.completionBlock with cachedEntity else fire completionBlock with error
} else {
//Nothing worked, just fire completionBlock with error
if (self.completionBlock) {
self.completionBlock(error,self);
self.completionBlock = nil;
}
}
}
@end
View Controller (Example)
GTViewControlller.m
#import "GTViewController.h"
//Models
#import "GTBaseEntity.h"
@interface GTViewController ()
@property (nonatomic, strong) GTBaseEntity *entity;
@end
@implementation GTViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// This is an example, merely to show how a GTBaseEntity might be used within a VC
[self refreshEntity];
}
- (void)refreshEntity
{
//Assume self.entity was injected on init and is valid
GTBaseEntity *currentDisplayedEntity = self.entity;
[currentDisplayedEntity fetch:^(NSError *errorOrNil, GTBaseEntity *returnedEntity) {
if (errorOrNil == nil) {
//Do something fun, like update the UI
} else {
//Alert the user that something is wacky.
}
}];
}
@end