Load View Controller even if one API fails - ios

I have three API's I pull data from, and put into a UITableView inside of my ViewController.m.
Is there a way to still let the UITableView load if one of the websites isn't loading?
Right now, the ViewController.m just doesn't load if all 3 sources aren't loading per my method.
Here's the method I use:
- (void)loadOneWithSuccess:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success
failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *tNE = [defaults objectForKey:[NSString stringWithFormat:#"tNE%#", bn]];
NSString *path = [NSString stringWithFormat:#"xx/%#/", tNE];
[self.eObjectManager getObjectsAtPath:path parameters:nil success:success failure:failure];
}
- (void)loadMedia {
self.combinedModel = [NSMutableArray array];
// Here's the #1
[self loadOneWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self.combinedModel addObjectsFromArray:mappingResult.array];
// Here's the trick. call API2 here. Doing so will serialize these two requests
[self loadTwoWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self.combinedModel addObjectsFromArray:mappingResult.array];
// Here's the trick. call API3 here. Doing so will serialize these two requests
[self loadThreeWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self.combinedModel addObjectsFromArray:mappingResult.array];
[self sortCombinedModel];
[self.tableView reloadData];
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"No?: %#", error);
}];
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"No?: %#", error);
}];
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"No?: %#", error);
}];
}
So if API1 doesn't load, API2 and API3 will still load and show in the UITableView in ViewController.m.

Maybe you can try something like this, first define tree bool variables: finish1, finish2 and finish3
- (void)loadMedia {
self.combinedModel = [NSMutableArray array];
[self loadOneWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self.combinedModel addObjectsFromArray:mappingResult.array];
finish1 = true;
[self reloadTableData]
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"No?: %#", error);
finish1 = true;
[self reloadTableData]
}];
[self loadTwoWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self.combinedModel addObjectsFromArray:mappingResult.array];
finish2 = true;
[self reloadTableData]
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"No?: %#", error);
finish2 = true;
[self reloadTableData]
}];
[self loadThreeWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self.combinedModel addObjectsFromArray:mappingResult.array];
finish2 = true;
[self reloadTableData]
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"No?: %#", error);
finish3 = true;
[self reloadTableData]
}];
}
- (void) reloadTableData {
if (finish1 && finish2 && finish3) {
[self sortCombinedModel];
[self.tableView reloadData];
}
}

The loadOne, loadTwo ... functions have a disadvantage which is that they take two block parameters, one for success and one for fail. If you change those to take a single block that handles success or failure, it will be much easier to carry on after errors occur.
EDIT Change how you call your eObjectManager by not directly passing on the completion and failure blocks. Instead, implement those blocks and rearrange the params to match the single block interface...
- (void)betterLoadOneWithCompletion:(void (^)(RKObjectRequestOperation*, RKMappingResult*, NSError *))completion {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *tNE = [defaults objectForKey:[NSString stringWithFormat:#"tNE%#", bn]];
NSString *path = [NSString stringWithFormat:#"xx/%#/", tNE];
[self.eObjectManager getObjectsAtPath:path parameters:nil success:^(RKObjectRequestOperation *op, RKMappingResult *map) {
// success! pass the operation, map result and no error
completion(op, map, nil);
} failure:^(RKObjectRequestOperation *op, NSError *error) {
// fail. pass the operation, no result and the error
completion(op, nil, error);
}];
}
It can still call your old function or some external library with two blocks, but it combines the result into a single block. The caller of this expects that they will either get a good RKMappingResult and a nil NSError, or a nil for the result parameter and an instance of an error. With this api, we can easily fix your method to just log errors as they occur and carry on, error or not...
- (void)loadMedia {
self.combinedModel = [NSMutableArray array];
// changed the loadOneWithCompletion signature to take just a single block, calling it on success or fail
[self betterLoadOneWithCompletion:^(RKObjectRequestOperation *op, RKMappingResult *mappingResult, NSError *error) {
// if it worked, handle the results
if (!error) {
[self.combinedModel addObjectsFromArray:mappingResult.array];
} else {
// if it didn't work, log the error, but execution continues
NSLog(#"No?: %#", error);
}
// even if it didn't work, we can keep going...
[self betterLoadOneWithCompletion:^(RKObjectRequestOperation *op, RKMappingResult *mappingResult, NSError *error) {
// same - handle results
if (!error) {
[self.combinedModel addObjectsFromArray:mappingResult.array];
} else {
// same - log the error if there is one
NSLog(#"No?: %#", error);
}
// same - log the error and keep going
[self betterLoadOneWithCompletion:^(RKObjectRequestOperation *op, RKMappingResult *mappingResult, NSError *error) {
// same...
if (!error) {
[self.combinedModel addObjectsFromArray:mappingResult.array];
} else {
NSLog(#"No?: %#", error);
}
[self sortCombinedModel];
[self.tableView reloadData];
}];
}];
}];
}

Related

How can I write completion block with nullable?

When I call this method with nil, the app crashes, but I want to know how to write it with nullable.
CRASH
[KPTaxnoteApiSaveHandler saveEntryWithUuid:uuid completion:nil];
OK
[KPTaxnoteApiSaveHandler saveEntryWithUuid:uuid completion:^(NSError *error) {}];
This is the code.
+ (void)saveEntryWithUuid:(NSString *)uuid completion:(void (^ __nullable)(NSError * _Nullable error))completion {
NSLog(#"saveEntryWithUuid");
Entry *entry = [Entry MR_findFirstByAttribute:#"uuid" withValue:uuid];
NSDictionary *params = #{#"entry[uuid]":entry.uuid};
[KPTaxnoteApiSaveHandler postWithUrl:kApiUrlStringForEntry params:params completion:^(NSError *error) {
if (!error) {
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
Entry *entry = [Entry MR_findFirstByAttribute:#"uuid" withValue:uuid inContext:localContext];
entry.needSave = #NO;
}];
}
completion(error);
}];
+ (void)postWithUrl:(NSString *)urlStr params:(NSDictionary *)params completion:(nullable void (^)(NSError *_Nullable error))completion {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager POST:urlStr parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
completion(nil);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
completion(error);
}];
Where is the crash happening? My first guess is you need to do something like this:
if (completion) {
completion(nil); // Or completion(error);
}
This will handle the case where the completion is nil.

AFNetworking 2.0 - how to pass response to another class on success from subclassed AFHTTPSessionManager

Beginner ios AFNetworking 2.0 Qns: Having subclassed AFHTTPSessionManager to something like "MyAPIManager" and placed my all my API calls (GET/POST/PUT etc.) in this custom manager class, I'm having problems making use of the response on request success in another class (say class B).
I know I can refactor this and pluck out the POST call portion to class B, so that I can dump the relevant class B methods in the callback, but this would get messy, especially with multiple API calls.
I want to pass this response (e.g. the returned objectId) to another class and right now I'm just using a NSNotification which class B listens for, but this still feels a bit 'hackish' and am wondering if there is a better way to do this.
Currently in MyAPIManager : AFHTTPSessionManager:
- (void) POSTRecordJson:(NSDictionary *)json
{
[self POST:#"classes/Record/" parameters:json success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"Posted JSON: %#", json.description);
if ([responseObject isKindOfClass:[NSDictionary class]]) {
NSLog(#"Response: %#", responseObject);
//Notify objectId received
[[NSNotificationCenter defaultCenter]
postNotificationName:#"ReceivedObjectIdNotification"
object:self
userInfo:responseObject];
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
And in Class B I've called:
MyApiManager *manager = [MyApiManager sharedInstance];
[manager POSTRecordJson:someJSONdict];
you could do 2 things.. by using a protocol/delegate or a block..
but i, personally, prefers block soo..
first make a block Datatype
typedef void(^SuccessBlock)(id success); example
and add the parameter with the block on it
- (void) POSTRecordJson:(NSDictionary *)json success:(SuccessBlock)success
{
[self POST:#"classes/Record/" parameters:json success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"Posted JSON: %#", json.description);
if ([responseObject isKindOfClass:[NSDictionary class]]) {
NSLog(#"Response: %#", responseObject);
//Notify objectId received
success(responseObject);
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
and to call the new function..
MyApiManager *manager = [MyApiManager sharedInstance];
[manager POSTRecordJson:someJSONdict success:^(id result){
NSDictionary *dictionary = (NSDictionary *)result;
NSLog(#"response: %#",dictionary)
}];
You would want to pass a completion block into your -POSTRecordJson: method.
For example, you would refactor your method to do the following:
- (void) POSTRecordJson:(NSDictionary *)json completion:(void(^)(BOOL success, id response, NSError *error))completion
{
[self POST:#"classes/Record/" parameters:json success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"Posted JSON: %#", json.description);
if ([responseObject isKindOfClass:[NSDictionary class]])
{
NSLog(#"Response: %#", responseObject);
if (completion) //if completion is NULL, calling it will crash your app so we always check that it is present.
{
completion(YES, responseObject, nil);
}
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"Error: %#", error);
if (completion)
{
completion(NO, nil, error);
}
}];
}
You could then handle this implementation like so:
//assuming `manager` and `dictionary` exist.
[manager POSTRecordJson:dictionary completion^(BOOL success, id response, NSError *error) {
if (success)
{
//do something with `response`
}
else
{
//do something with `error`
}
}];
However, if you are a beginner with AFNetworking and you want to adopt a great structure for handling web services, you should check out this excellent blog post.
You can use blocks to send the response back to the class after the response received from the server:
- (void) POSTRecordJson:(NSDictionary *)json response:(void (^)(id response, NSError *error))responseBlock
{
[self POST:#"classes/Record/" parameters:json success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"Posted JSON: %#", json.description);
if ([responseObject isKindOfClass:[NSDictionary class]]) {
NSLog(#"Response: %#", responseObject);
responseBlock(responseObject, nil);
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"Error: %#", error);
responseBlock(nil, error);
}];
}

AFNetworking completion block wait until done

I'm using AFNetworking for POST requests. I need to wait until the completion block is done to return data, and I've run into problems.
I had a solution that was working until I switched to AFNetworking:
int i = 0;
while (!done)
{
[NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
i++;
NSLog(#"While Loop:%i", i);
if (i == 149) //request timed out
done = YES;
}
Now, the solution works sporadically. Sometimes it completes with NSLog(#"While Loop:%i", i); only logging 1 or 2, but sometimes it logs until 149 and times out. It seems like the NSRunLoop sometimes runs in a different thread, but sometimes runs on the same thread blocking my request.
Here's the code that I currently have:
- (id)postRequestWithBaseURLString:(NSString *)baseURLString function:(NSString *)function parameters:(NSDictionary *)parameters
{
if (baseURLString)
function = [baseURLString stringByAppendingString:function];
__block BOOL done = NO;
__block id returnObject = nil;
[self.manager POST:function parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSLog(#"Success: %#", responseObject);
done = YES;
returnObject = responseObject;
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(#"Error: %#", error);
done = YES;
}];
[manager.operationQueue waitUntilAllOperationsAreFinished];
int i = 0;
while (!done)
{
[NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
i++;
NSLog(#"While Loop:%i", i);
if (i == 149) //request timed out
done = YES;
}
return returnObject;
}
I've tried doing a dispatch_group without success. Help?
EDIT
Ok, I've got more info. My solution works when I first call the API (for example, when I first call a ViewController), but not afterwards. Perhaps, after the view is loaded, the while loop is called on the same thread as the API call, therefore blocking it?
This also seems likely, since the NSLog(#"Success: %#", responseObject); is called almost exactly after timeout happens.
Found an elegant solution online. I created my own completion block. Here's what I did:
+ (void)jsonRequestWithBaseURL:(NSString *)baseURL function:(NSString *)function parameters:(NSDictionary *)parameters completion:(void (^)(NSDictionary *json, BOOL success))completion
{
if (baseURL)
function = [baseURL stringByAppendingString:function];
NSLog(#"%# function:%#, parameters:%#", self.class, function, parameters);
[AFHTTPRequestOperationManager.manager POST:function parameters:parameters success:^(AFHTTPRequestOperation *operation, id jsonObject)
{
//NSLog(#"Success: %#", jsonObject);
NSDictionary *jsonDictionary = (NSDictionary *)jsonObject;
if (completion)
completion(jsonDictionary, YES);
} failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(#"%# AFError: %#", self.class, [error localizedDescription]);
completion(nil, NO);
}];
}
Then, when I need to call the method, I do this:
NSDictionary *parameters = #{#"email":#"me#example.com", #"password":#"mypassword"};
[ZAPRootViewController jsonRequestWithBaseURL:#"http://www.myserver.com/" function:#"login.php" parameters:parameters completion:^(NSDictionary *json, BOOL success)
{
if (success)
{
//do something
}
}];

Assign NSArray from a ViewController to go through a client

I have a NSArray in my ViewController called tweets. To retrieve the tweets I have to use a method in my TwitterClient.m class. The method is shown below:
-(NSArray*)getTimeline {
NSArray *timelineArray;
[self.twitterClient registerHTTPOperationClass:[AFJSONRequestOperation class]];
[self.twitterClient getPath:#"1.1/statuses/user_timeline.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSArray *responseArray = (NSArray *)responseObject;
[responseArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(#"Success: %#", obj);
}];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
return responseArray;
}
However as you know you cant return the responseArray because it is inside a block. What would be a more efficient way to do this.
I have another way but in this method I cannot assign the response array as the NSArray that is the parameter:
-(void)getTimeline:(NSArray*)tweetArray {
[self.twitterClient registerHTTPOperationClass:[AFJSONRequestOperation class]];
[self.twitterClient getPath:#"1.1/statuses/user_timeline.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSArray *responseArray = (NSArray *)responseObject;
[responseArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(#"Success: %#", obj);
responseArray = tweetArray;
}];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
Basically the final question is how do I assign the tweets array in my View Controller to equal the response array that comes in through the TwitterClient class.
You should pass a block that is getting called after you are done getting your required information:
- (void)getTimelineWithCompletionBlock:(void (^)(NSError *err, NSArray *arr)) block {
[self.twitterClient registerHTTPOperationClass:[AFJSONRequestOperation class]];
[self.twitterClient getPath:#"1.1/statuses/user_timeline.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
(block ? block(nil, responseObject) : nil);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
(block ? block(error, nil) : nil);
}];
}
You can call the method the following way:
[self getTimelineWithCompletionBlock:^(NSError *err, NSArray *arr) {
}];
Inside the completion block you have the data available and you can update your UI or whatever you intend to do. Depending on the implementation of getPath, you maybe have to dispatch the completion block on the main queue, because you are just allowed to update UI on the main thread.

AFOAuth2Client and refresh token

How to implement the Oauth in iPad application?
How does AFOAuth2Client manages refreshing token mechanism in oauth 2.0?
Is there any method to implement it inside the class or do we have to implement it in our own way? How to check the token is expired or not?
The way that I have solved this is to wrap all my requests with a code block which will refresh the access token if needed e.g.
Add some typedefs for success and failure blocks:
typedef void (^YFRailsSaasApiClientSuccess)(AFJSONRequestOperation *operation, id responseObject);
typedef void (^YFRailsSaasApiClientFailure)(AFJSONRequestOperation *operation, NSError *error);
Then the request method is:
- (void)getProductsWithSuccess:(YFRailsSaasApiClientSuccess)success failure:(YFRailsSaasApiClientFailure)failure {
NSLog(#"getProductsWithSuccess");
success = ^(AFJSONRequestOperation *operation, id responseObject) {
[self getPath:#"api/1/products"
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"getProductsWithSuccess: success");
// TODO: handle response
if (success) {
success((AFJSONRequestOperation *)operation, responseObject);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"getProductsWithSuccess: failure");
if (failure) {
failure((AFJSONRequestOperation *)operation, error);
}
}];
};
[self refreshAccessTokenWithSuccess:success failure:failure];
}
And the method which checks for token expiry and refreshes it if needed is:
- (void)refreshAccessTokenWithSuccess:(YFRailsSaasApiClientSuccess)success failure:(YFRailsSaasApiClientFailure)failure {
NSLog(#"refreshAccessTokenWithSuccess");
if (self.credential == nil) {
if (failure) {
NSMutableDictionary *errorDetail = [NSMutableDictionary dictionary];
[errorDetail setValue:#"Failed to get credentials" forKey:NSLocalizedDescriptionKey];
NSError *error = [NSError errorWithDomain:#"world" code:200 userInfo:errorDetail];
failure(nil, error);
}
return;
}
if (!self.credential.isExpired) {
NSLog(#"refreshAccessTokenWithSuccess: credential has not expired");
if (success) {
success(nil, nil);
}
return;
}
NSLog(#"refreshAccessTokenWithSuccess: refreshing credential");
[self authenticateUsingOAuthWithPath:#"oauth/token"
refreshToken:self.credential.refreshToken
success:^(AFOAuthCredential *newCredential) {
NSLog(#"Successfully refreshed OAuth credentials %#", newCredential.accessToken);
self.credential = newCredential;
[AFOAuthCredential storeCredential:newCredential
withIdentifier:self.serviceProviderIdentifier];
if (success) {
success(nil, nil);
}
}
failure:^(NSError *error) {
NSLog(#"An error occurred refreshing credential: %#", error);
if (failure) {
failure(nil, error);
}
}];
}
Full source code is up on GitHub: https://github.com/yellowfeather/rails-saas-ios.

Resources