Skip to content

Instantly share code, notes, and snippets.

@HHuckebein
Created June 10, 2013 09:33
Show Gist options
  • Save HHuckebein/5747552 to your computer and use it in GitHub Desktop.
Save HHuckebein/5747552 to your computer and use it in GitHub Desktop.
A pattern for implementing a class managing a NSOperationQueue and NSOperations. The files attached show how to apply this pattern to downloading gists/gravatar images from github.com
//
// GistCommunicationManager.h
// GistExplorer
//
// Created by Bernd Rabe on 28.05.13.
// Copyright (c) 2013 RABE_IT Services. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "OperationDelegate.h"
#import "ProviderDelegate.h"
@interface GistCommunicationManager : NSObject
+ (instancetype)sharedManger;
- (void)loadContentForProvider:(id <ProviderDelegate>)provider success:(void(^)())sucess failure:(OperationFailure)failure;
- (void)loadGravatarForProvider:(id <ProviderDelegate>)provider avatarURL:(NSURL *)avatarURL success:(OperationSuccessWithData)success;
- (void)cancelAllOperationsForProvider:(id)provider;
- (void)cancelOperationForKey:(NSString *)key;
@end
//
// GistCommunicationManager.m
// GistExplorer
//
// Created by Bernd Rabe on 28.05.13.
// Copyright (c) 2013 RABE_IT Services. All rights reserved.
//
#import "GistCommunicationManager.h"
#import "JSONOperation.h"
#import "GravatarOperation.h"
@interface GistCommunicationManager()
@property (nonatomic, strong) NSOperationQueue *operationQueue;
@property (nonatomic, strong) NSMutableDictionary *operations;
@end
@implementation GistCommunicationManager
+ (instancetype)sharedManger
{
static GistCommunicationManager *sharedManger = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManger = [[GistCommunicationManager alloc] init];
});
return sharedManger;
}
- (NSOperationQueue *)operationQueue
{
if (nil == _operationQueue) {
_operationQueue = [[NSOperationQueue alloc] init];
_operationQueue.name = @"Background Processing Queue";
}
return _operationQueue;
}
- (NSMutableDictionary *)operations
{
if (nil == _operations) {
_operations = [NSMutableDictionary dictionary];
}
return _operations;
}
- (void)loadContentForProvider:(id <ProviderDelegate>)provider success:(void(^)())sucess failure:(OperationFailure)failure
{
JSONOperation *operation = [JSONOperation jsonOperationForProvider:provider
success:^(NSOperation *operation, NSString *objectNotation) {
[self.operations removeObjectForKey:[(JSONOperation *)operation operationKey]];
NSError *error = nil;
[provider processData:objectNotation error:&error];
if (nil == error) {
if (sucess) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
sucess();
}];
}
}
else {
if(failure) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
failure(operation, error);
}];
}
}
}
failure:failure];
[self.operations setObject:operation forKey:[operation operationKey]];
[self.operationQueue addOperation:operation];
}
- (void)loadGravatarForProvider:(id <ProviderDelegate>)provider avatarURL:(NSURL *)avatarURL success:(OperationSuccessWithData)success
{
GravatarOperation *operation = [GravatarOperation gravatarOperationForProvider:provider
avatarURL:avatarURL
success:^(NSOperation *operation, NSData *data) {
[self.operations removeObjectForKey:[(GravatarOperation *)operation operationKey]];
if (success) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
success(operation, data);
}];
}
}];
[self.operations setObject:operation forKey:[operation operationKey]];
[self.operationQueue addOperation:operation];
}
- (void)cancelAllOperationsForProvider:(id)provider
{
[self.operationQueue cancelAllOperations];
NSMutableArray *keys = [NSMutableArray array];
[self.operations enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if ([obj provider] == provider) {
[obj cancelOperation];
[keys addObject:key];
}
}];
[self.operations removeObjectsForKeys:keys];
}
- (void)cancelOperationForKey:(NSString *)key
{
id operation = self.operations[key];
if (operation && [operation conformsToProtocol:@protocol(OperationDelegate)]) {
[operation cancelOperation];
[self.operations removeObjectForKey:key];
}
}
@end
//
// GistProvider.h
// GistExplorer
//
// Created by Bernd Rabe on 28.05.13.
// Copyright (c) 2013 RABE_IT Services. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "ProviderDelegate.h"
extern NSString *const gistsCellReuseIdentifier;
extern NSString *const GistsProviderErrorDomain;
extern NSString *const GistsProviderDidSelectGistNotification;
NS_ENUM(NSUInteger, GistsProviderError) {
GistsProviderInvalidJSONError
};
@interface GistsProvider : NSObject <UITableViewDataSource, UITableViewDelegate, ProviderDelegate>
- (id)initWithURl:(NSURL *)url;
@end
//
// GistProvider.m
// GistExplorer
//
// Created by Bernd Rabe on 28.05.13.
// Copyright (c) 2013 RABE_IT Services. All rights reserved.
//
#import "GistsProvider.h"
#import "Gist.h"
#import "User.h"
#import "GistCommunicationManager.h"
NSString *const gistsCellReuseIdentifier = @"SubtitleStyle";
NSString *const GistsProviderErrorDomain = @"GistsProviderErrorDomain";
NSString *const GistsProviderDidSelectGistNotification = @"GistsProviderDidSelectGistNotification";
@interface GistsProvider()
@property (nonatomic, strong) NSArray *data;
@property (nonatomic, strong) NSURL *contentURL;
@property (nonatomic, strong) UIImage *johnDoeImage;
@property (nonatomic, strong) NSCache *avatarImageCache;
@end
@implementation GistsProvider
- (id)initWithURl:(NSURL *)url
{
self = [super init];
if (self) {
self.contentURL = url;
}
return self;
}
#pragma mark - DataSource Part
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSParameterAssert(section == 0);
return self.data.count;
}
- (Gist *)gistForIndexPath:(NSIndexPath *)indexPath
{
return self.data[indexPath.row];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSParameterAssert(indexPath.section == 0);
NSParameterAssert(indexPath.row < self.data.count);
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:gistsCellReuseIdentifier forIndexPath:indexPath];
Gist *gist = [self gistForIndexPath:indexPath];
cell.textLabel.text = [gist titleText];
cell.detailTextLabel.text = [gist subtitleText];
if ([gist hasUser]) {
NSData *gravatarData = [self.avatarImageCache objectForKey:gist.user.gravatarID];
if (gravatarData) {
cell.imageView.image = [UIImage imageWithData:gravatarData];
}
else {
__weak typeof(self) weakSelf = self;
[[GistCommunicationManager sharedManger] loadGravatarForProvider:(GistsProvider *)tableView.dataSource
avatarURL:gist.user.avatarURL
success:^(NSOperation *operation, NSData *data) {
[weakSelf.avatarImageCache setObject:data forKey:gist.user.gravatarID];
UIImage *image = [UIImage imageWithData:data];
[tableView cellForRowAtIndexPath:indexPath].imageView.image = image;
}];
cell.imageView.image = self.johnDoeImage;
}
}
return cell;
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *operationKey = [[self gistForIndexPath:indexPath] operationKey];
if (operationKey) {
[[GistCommunicationManager sharedManger] cancelOperationForKey:operationKey];
}
}
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row % 2 == 0) {
cell.backgroundColor = [UIColor colorWithRed:163/255. green:204/255. blue:20/255. alpha:1.];
}
}
#pragma mark - Delegate Part
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
{
Gist *gist = [self gistForIndexPath:indexPath];
NSNotification *notification = [NSNotification notificationWithName:GistsProviderDidSelectGistNotification object:gist];
[[NSNotificationCenter defaultCenter] postNotification:notification];
}
#pragma mark - Lazy Init
- (UIImage *)johnDoeImage
{
if (nil == _johnDoeImage) {
_johnDoeImage = [UIImage imageNamed:@"JohnDoe.png"];
}
return _johnDoeImage;
}
- (NSCache *)avatarImageCache
{
if (nil == _avatarImageCache) {
_avatarImageCache = [[NSCache alloc] init];
}
return _avatarImageCache;
}
#pragma mark - ProviderDelegate
- (NSString *)title
{
return @"Public Gist's";
}
- (void)processData:(NSString *)objectNotation error:(NSError **)error
{
NSData *unicodeNotation = [objectNotation dataUsingEncoding:NSUTF8StringEncoding];
NSError *localError = nil;
id jsonObject = [NSJSONSerialization JSONObjectWithData:unicodeNotation
options:NSJSONReadingAllowFragments
error:&localError];
NSDictionary *parsedObject = (id)jsonObject;
if (nil == parsedObject) {
if (error != NULL) {
*error = [NSError errorWithDomain:GistsProviderErrorDomain code:GistsProviderInvalidJSONError userInfo:nil];
}
}
else {
NSMutableArray *gistsArray = [NSMutableArray array];
for (NSDictionary *item in parsedObject) {
Gist *gist = [[Gist alloc] init];
if (item[@"id"]) {
gist.gistID = [item[@"id"] integerValue];
}
if (item[@"created_at"]) {
gist.createdAt = [gist dateFromISO8601String:item[@"created_at"]];
}
if (item[@"description"] && item[@"description"] != [NSNull null]) {
gist.descriptionText = item[@"description"];
}
if (item[@"url"]) {
gist.jsonURL = [NSURL URLWithString:item[@"url"]];
}
if (item[@"user"] != [NSNull null]) {
gist.user = [User userFromUserDictionary:item[@"user"]];
}
[gistsArray addObject:gist];
}
self.data = [gistsArray copy];
}
}
- (void)removeAllData
{
_data = nil;
}
@end
//
// GravatarOperation.h
// GistExplorer
//
// Created by Bernd Rabe on 28.05.13.
// Copyright (c) 2013 RABE_IT Services. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "OperationDelegate.h"
@interface GravatarOperation : NSOperation <NSURLConnectionDataDelegate, OperationDelegate>
+ (instancetype)gravatarOperationForProvider:(id)provider avatarURL:(NSURL *)avatarURL success:(OperationSuccessWithData)success;
@end
//
// GravatarOperation.m
// GistExplorer
//
// Created by Bernd Rabe on 28.05.13.
// Copyright (c) 2013 RABE_IT Services. All rights reserved.
//
#import "GravatarOperation.h"
#import "ProviderDelegate.h"
@interface GravatarOperation()
@property (nonatomic, assign) BOOL executing;
@property (nonatomic, assign) BOOL finished;
@property (nonatomic, assign) BOOL cancelled;
@property (nonatomic, strong) NSURL *url;
@property (nonatomic, weak) id <ProviderDelegate> delegate;
@property (nonatomic, strong) NSURLConnection *connection;
@property (nonatomic, strong) NSMutableData *receivedData;
@property (nonatomic, copy) OperationSuccessWithData success;
@end
@implementation GravatarOperation
- (void)start
{
if ([self isCancelled])
{
[self willChangeValueForKey:@"isFinished"];
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
NSURLRequest *request = [NSURLRequest requestWithURL:self.url];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[self.connection start];
}
+ (instancetype)gravatarOperationForProvider:(id)provider avatarURL:(NSURL *)avatarURL success:(OperationSuccessWithData)success
{
NSAssert(nil != provider, @"provider parameter can't be nil");
GravatarOperation *operation = [[[self class] alloc] init];
operation.delegate = provider;
operation.url = avatarURL;
operation.success = success;
return operation;
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return _executing;
}
- (BOOL)isFinished {
return _finished;
}
- (BOOL)isCancelled {
return _cancelled;
}
- (NSMutableData *)receivedData
{
if (nil == _receivedData) {
_receivedData = [NSMutableData data];
}
return _receivedData;
}
#pragma mark - NSURLConnectionDataDelegate
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if (_success) {
self.success(self, [self.receivedData copy]);
}
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_executing = NO;
_finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.receivedData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[connection cancel];
_connection = nil;
_receivedData = nil;
_url = nil;
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_executing = NO;
_finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
#pragma mark - OperationDelegate
- (NSString *)operationKey
{
return [self.url absoluteString];
}
- (id)provider
{
return _delegate;
}
- (void)cancelOperation
{
_success = nil;
[self.connection cancel];
_connection = nil;
_receivedData = nil;
_url = nil;
[self willChangeValueForKey:@"isCancelled"];
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_executing = NO;
_finished = YES;
_cancelled = YES;
[self didChangeValueForKey:@"isCancelled"];
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
@end
//
// JSONOperation.h
// GistExplorer
//
// Created by Bernd Rabe on 28.05.13.
// Copyright (c) 2013 RABE_IT Services. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "OperationDelegate.h"
@interface JSONOperation : NSOperation <NSURLConnectionDataDelegate, OperationDelegate>
+ (instancetype)jsonOperationForProvider:(id)provider success:(OperationSuccessWithString)success failure:(OperationFailure)failure;
@end
//
// JSONOperation.m
// GistExplorer
//
// Created by Bernd Rabe on 28.05.13.
// Copyright (c) 2013 RABE_IT Services. All rights reserved.
//
#import "JSONOperation.h"
#import "ProviderDelegate.h"
@interface JSONOperation()
@property (nonatomic, assign) BOOL executing;
@property (nonatomic, assign) BOOL finished;
@property (nonatomic, assign) BOOL cancelled;
@property (nonatomic, strong) NSURL *url;
@property (nonatomic, weak) id <ProviderDelegate> delegate;
@property (nonatomic, strong) NSURLConnection *connection;
@property (nonatomic, strong) NSMutableData *receivedData;
@property (nonatomic, copy) OperationFailure failure;
@property (nonatomic, copy) OperationSuccessWithString success;
@end
@implementation JSONOperation
- (void)start
{
if ([self isCancelled])
{
[self willChangeValueForKey:@"isFinished"];
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
NSURLRequest *request = [NSURLRequest requestWithURL:self.url];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[self.connection start];
[self willChangeValueForKey:@"isExecuting"];
_executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (NSMutableData *)receivedData
{
if (nil == _receivedData) {
_receivedData = [NSMutableData data];
}
return _receivedData;
}
+ (instancetype)jsonOperationForProvider:(id <ProviderDelegate>)provider success:(OperationSuccessWithString)success failure:(OperationFailure)failure
{
NSAssert(nil != provider, @"provider parameter can't be nil");
JSONOperation *operation = [[[self class] alloc] init];
operation.delegate = provider;
operation.url = provider.contentURL;
operation.failure = failure;
operation.success = success;
return operation;
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return _executing;
}
- (BOOL)isFinished {
return _finished;
}
- (BOOL)isCancelled {
return _cancelled;
}
#pragma mark - NSURLConnectionDataDelegate
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if (_success) {
NSString *receivedText = [[NSString alloc] initWithData:self.receivedData encoding:NSUTF8StringEncoding];
_receivedData = nil;
self.success(self, receivedText);
}
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_executing = NO;
_finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.receivedData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[connection cancel];
_connection = nil;
_receivedData = nil;
_url = nil;
if (_failure) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.failure(self, error);
}];
}
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_executing = NO;
_finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
#pragma mark - OperationDelegate
- (NSString *)operationKey
{
return [self.url absoluteString];
}
- (id)provider
{
return _delegate;
}
- (void)cancelOperation
{
_failure = nil;
_success = nil;
[self.connection cancel];
_connection = nil;
_receivedData = nil;
_url = nil;
[self willChangeValueForKey:@"isCancelled"];
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_executing = NO;
_finished = YES;
_cancelled = YES;
[self didChangeValueForKey:@"isCancelled"];
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
@end
//
// OperationDelegate.h
// GistExplorer
//
// Created by Bernd Rabe on 28.05.13.
// Copyright (c) 2013 RABE_IT Services. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef void(^OperationSuccessWithData)(NSOperation *operation, NSData *data);
typedef void(^OperationSuccessWithString)(NSOperation *operation, NSString *objectNotation);
typedef void(^OperationFailure)(NSOperation *operation, NSError *error);
@protocol OperationDelegate <NSObject>
- (id)provider;
- (NSString *)operationKey;
- (void)cancelOperation;
@end
//
// ProviderDelegate.h
// GistExplorer
//
// Created by Bernd Rabe on 28.05.13.
// Copyright (c) 2013 RABE_IT Services. All rights reserved.
//
#import <Foundation/Foundation.h>
@protocol ProviderDelegate <NSObject>
- (NSString *)title;
- (NSURL *)contentURL;
- (void)processData:(NSString *)objectNotation error:(NSError **)error;
- (void)removeAllData;
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment