Created
July 20, 2015 03:08
-
-
Save jparishy/a6110f4202f8fb2a0202 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// JLEInstagramAPI.m | |
// WhenToPost | |
// | |
// Created by Julius Parishy on 6/27/15. | |
// Copyright (c) 2015 Julius Parishy. All rights reserved. | |
// | |
#import "JLEInstagramAPI.h" | |
#import <SSKeychain/SSKeychain.h> | |
static NSString *const JLEKeychainInstagramService = @"Instagram"; | |
static NSString *const JLEKeychainInstagramAccount = @"StoredAccessToken"; | |
NSString *const JLEInstagramAPIErrorDomain = @"JLEInstagramAPIErrorDomain"; | |
NSString *const JLEInstagramAPIErrorUserInfoURLRequestKey = @"JLEInstagramAPIErrorUserInfoURLRequestKey"; | |
NSString *const JLEInstagramAPIErrorUserInfoHTTPResponseKey = @"JLEInstagramAPIErrorUserInfoHTTPResponseKey"; | |
NSString *const JLEInstagramAPIErrorUserInfoResponseDataKey = @"JLEInstagramAPIErrorUserInfoResponseDataKey"; | |
typedef NS_ENUM(NSInteger, JLEHTTPMethod) | |
{ | |
JLEHTTPMethodGET, | |
JLEHTTPMethodPOST, | |
}; | |
static NSString *JLEHTTPMethodStringForMethod(JLEHTTPMethod method); | |
@interface JLERequestResponse : NSObject | |
@property (nonatomic, strong, readonly) NSHTTPURLResponse *URLResponse; | |
@property (nonatomic, strong, readonly) id responseObject; | |
- (instancetype)initWithURLResponse:(NSHTTPURLResponse *)URLResponse responseObject:(id)responseObject; | |
@end | |
@interface JLEInstagramAPI () | |
@property (nonatomic, strong) NSURL *baseURL; | |
@end | |
@implementation JLEInstagramAPI | |
- (instancetype)initWithAccessToken:(NSString *)accessToken | |
{ | |
if((self = [super init])) | |
{ | |
_accessToken = accessToken; | |
_queue = [[NSOperationQueue alloc] init]; | |
_baseURL = [NSURL URLWithString:@"https://api.instagram.com/v1/"]; | |
} | |
return self; | |
} | |
+ (NSString *)storedAccessToken | |
{ | |
return [SSKeychain passwordForService:JLEKeychainInstagramService account:JLEKeychainInstagramAccount]; | |
} | |
+ (void)setStoredAccessToken:(NSString *)accessToken | |
{ | |
[SSKeychain setPassword:accessToken forService:JLEKeychainInstagramService account:JLEKeychainInstagramAccount]; | |
} | |
- (NSDictionary *)defaultHTTPHeaders | |
{ | |
return @{ | |
@"Accept" : @"application/json", | |
@"Content-Type" : @"application/json" | |
}; | |
} | |
- (NSString *)queryParametersStringFromParameters:(NSDictionary *)parameters | |
{ | |
NSMutableDictionary *allParameters = [parameters mutableCopy] ?: [NSMutableDictionary dictionary]; | |
allParameters[@"access_token"] = self.accessToken; | |
NSString *string = @""; | |
NSArray *keys = [[allParameters allKeys] sortedArrayUsingDescriptors:@[ [NSSortDescriptor sortDescriptorWithKey:@"self" ascending:YES] ]]; | |
for(NSString *key in keys) | |
{ | |
id value = allParameters[key]; | |
NSString *maybeAmp = (keys.lastObject == key) ? @"" : @"&"; | |
string = [string stringByAppendingFormat:@"%@=%@%@", key, value, maybeAmp]; | |
} | |
return string; | |
} | |
- (NSURL *)APIEndpointURLWithPath:(NSString *)path | |
method:(JLEHTTPMethod)method | |
parameters:(NSDictionary *)parameters | |
{ | |
switch(method) | |
{ | |
case JLEHTTPMethodGET: | |
{ | |
NSString *queryParamtersString = [self queryParametersStringFromParameters:parameters]; | |
NSString *pathWithParameters = [path stringByAppendingFormat:@"?%@", queryParamtersString]; | |
return [NSURL URLWithString:pathWithParameters relativeToURL:self.baseURL]; | |
} | |
case JLEHTTPMethodPOST: | |
{ | |
return [NSURL URLWithString:path relativeToURL:self.baseURL]; | |
} | |
}; | |
} | |
- (NSURLRequest *)requestWithPath:(NSString *)path | |
method:(JLEHTTPMethod)method | |
parameters:(NSDictionary *)parameters | |
error:(NSError **)error | |
{ | |
NSURL *URL = [self APIEndpointURLWithPath:path method:method parameters:parameters]; | |
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:URL]; | |
request.HTTPMethod = JLEHTTPMethodStringForMethod(method); | |
NSDictionary *defaultHTTPHeaders = [self defaultHTTPHeaders]; | |
for(NSString *name in defaultHTTPHeaders) | |
{ | |
NSString *value = defaultHTTPHeaders[name]; | |
[request setValue:value forHTTPHeaderField:name]; | |
} | |
switch (method) | |
{ | |
case JLEHTTPMethodPOST: | |
{ | |
NSError *JSONError = nil; | |
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:parameters options:kNilOptions error:&JSONError]; | |
if(bodyData == nil) | |
{ | |
*error = JSONError; | |
return nil; | |
} | |
request.HTTPBody = bodyData; | |
}; | |
default: | |
break; | |
}; | |
return [request copy]; | |
} | |
- (BFTask *)beginRequestWithPath:(NSString *)path method:(JLEHTTPMethod)method parameters:(NSDictionary *)parameters | |
{ | |
BFTaskCompletionSource *source = [BFTaskCompletionSource taskCompletionSource]; | |
NSError *error = nil; | |
NSURLRequest *request = [self requestWithPath:path method:method parameters:parameters error:&error]; | |
if(request == nil) | |
{ | |
[source setError:error]; | |
} | |
else | |
{ | |
[NSURLConnection sendAsynchronousRequest:request queue:self.queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { | |
if(connectionError != nil) | |
{ | |
[source setError:connectionError]; | |
return; | |
} | |
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response; | |
if((HTTPResponse.statusCode / 100) != 2) | |
{ | |
NSError *statusCodeError = [NSError errorWithDomain:JLEInstagramAPIErrorDomain code:JLEInstagramAPIErrorCodeInvalidResponseStatusCode userInfo:@{ | |
JLEInstagramAPIErrorUserInfoURLRequestKey : request, | |
JLEInstagramAPIErrorUserInfoHTTPResponseKey : HTTPResponse, | |
JLEInstagramAPIErrorUserInfoResponseDataKey : data | |
}]; | |
[source setError:statusCodeError]; | |
return; | |
} | |
NSString *mimeType = HTTPResponse.MIMEType; | |
if([mimeType isEqualToString:@"application/json"] == NO) | |
{ | |
NSError *contentTypeError = [NSError errorWithDomain:JLEInstagramAPIErrorDomain code:JLEInstagramAPIErrorCodeInvalidContentType userInfo:@{ | |
JLEInstagramAPIErrorUserInfoURLRequestKey : request, | |
JLEInstagramAPIErrorUserInfoHTTPResponseKey : HTTPResponse, | |
JLEInstagramAPIErrorUserInfoResponseDataKey : data | |
}]; | |
[source setError:contentTypeError]; | |
return; | |
} | |
NSError *JSONError = nil; | |
id responseObject = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&JSONError]; | |
if(responseObject == nil) | |
{ | |
[source setError:JSONError]; | |
return; | |
} | |
[source setResult:@[ HTTPResponse, responseObject ]]; | |
}]; | |
} | |
return source.task; | |
} | |
- (BFTask *)GET:(NSString *)path parameters:(NSDictionary *)parameters | |
{ | |
return [self beginRequestWithPath:path method:JLEHTTPMethodGET parameters:parameters]; | |
} | |
- (BFTask *)POST:(NSString *)path parameters:(NSDictionary *)parameters | |
{ | |
return [self beginRequestWithPath:path method:JLEHTTPMethodPOST parameters:parameters]; | |
} | |
- (BFTask *)requestAllMedia | |
{ | |
return [self requestMediaWithPath:@"users/self/media/recent" nextMaxID:nil models:nil]; | |
} | |
- (BFTask *)requestMediaWithPath:(NSString *)path nextMaxID:(NSString *)nextMaxID models:(NSArray *)models | |
{ | |
NSDictionary *parameters = nil; | |
if(nextMaxID != nil) | |
{ | |
parameters = @{ | |
@"max_id" : nextMaxID | |
}; | |
} | |
return [[self GET:path parameters:parameters] continueWithBlock:^id(BFTask *task) { | |
if(task.error != nil) | |
{ | |
return [BFTask taskWithError:task.error]; | |
} | |
NSDictionary *rootObject = [self JSONObjectFromTask:task]; | |
NSArray *JSONArray = rootObject[@"data"]; | |
NSError *serializationError = nil; | |
NSArray *newModels = [MTLJSONAdapter modelsOfClass:[JLEInstagramMedia class] fromJSONArray:JSONArray error:&serializationError]; | |
if(newModels == nil) | |
{ | |
return [BFTask taskWithError:serializationError]; | |
} | |
NSArray *allModels = [models arrayByAddingObjectsFromArray:newModels] ?: newModels; | |
NSDictionary *pagination = rootObject[@"pagination"]; | |
NSString *nextURLString = pagination[@"next_url"]; | |
NSURL *nextURL = [NSURL URLWithString:nextURLString]; | |
if(nextURL != nil) | |
{ | |
NSString *path = nextURL.path; | |
NSString *nextMaxID = nil; | |
NSURLComponents *components = [NSURLComponents componentsWithURL:nextURL resolvingAgainstBaseURL:NO]; | |
NSArray *queryItems = components.queryItems; | |
for(NSURLQueryItem *queryItem in queryItems) | |
{ | |
if([queryItem.name isEqualToString:@"max_id"]) | |
{ | |
nextMaxID = queryItem.value; | |
break; | |
} | |
} | |
return [self requestMediaWithPath:path nextMaxID:nextMaxID models:allModels]; | |
} | |
else | |
{ | |
BFTaskCompletionSource *source = [BFTaskCompletionSource taskCompletionSource]; | |
[source setResult:allModels]; | |
return source.task; | |
} | |
}]; | |
} | |
- (NSHTTPURLResponse *)responseFromTask:(BFTask *)task | |
{ | |
NSArray *tuple = task.result; | |
return tuple[0]; | |
} | |
- (id)JSONObjectFromTask:(BFTask *)task | |
{ | |
NSArray *tuple = task.result; | |
return tuple[1]; | |
} | |
@end | |
@implementation JLERequestResponse | |
- (instancetype)initWithURLResponse:(NSHTTPURLResponse *)URLResponse responseObject:(id)responseObject | |
{ | |
if((self = [super init])) | |
{ | |
_URLResponse = URLResponse; | |
_responseObject = responseObject; | |
} | |
return self; | |
} | |
@end | |
static NSString *JLEHTTPMethodStringForMethod(JLEHTTPMethod method) | |
{ | |
switch(method) | |
{ | |
case JLEHTTPMethodGET: | |
{ | |
return @"GET"; | |
} | |
case JLEHTTPMethodPOST: | |
{ | |
return @"POST"; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
__weak typeof(self) weakSelf = self; | |
[[self.API requestAllMedia] continueWithBlock:^id(BFTask *task) { | |
__weak typeof(self) self = weakSelf; | |
[[NSOperationQueue mainQueue] addOperationWithBlock:^{ | |
__weak typeof(self) self = weakSelf; | |
[UIView animateWithDuration:0.4 animations:^{ | |
self.analyzingLabel.alpha = 0.0f; | |
}]; | |
[UIView animateWithDuration:1.2 delay:0.0 usingSpringWithDamping:0.5 initialSpringVelocity:1.0f options:kNilOptions animations:^{ | |
self.separatorView.transform = CGAffineTransformMakeScale(1.0f, 1.0f); | |
} completion:^(BOOL finished) { | |
__weak typeof(self) self = weakSelf; | |
self.analyzing = NO; | |
}]; | |
}]; | |
NSArray *unsorted = task.result; | |
NSDictionary *byWeekday = [JLEInstagramMedia groupedByWeekday:unsorted]; | |
[self handleGroupedByWeekday:byWeekday]; | |
NSDictionary *byHour = [JLEInstagramMedia groupedByHour:unsorted]; | |
[self handleGroupedByHour:byHour]; | |
return nil; | |
}]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment