How can I write completion block with nullable? - ios

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.

Related

UnitTest a Service with AFNetworking 3.x

I do want to test my Service that calls a method that uses AFNetworking 3.x.
Service:
+ (AnyPromise *)allRepositoriesfetchRepositoriesByLanguage:(NSString *)language forPage:(int)page {
return [[APIClient sharedClient] fetchRepositoriesByLanguage:language forPage:page].then(^(NSDictionary *response) {
NSValueTransformer *transformer = [MTLJSONAdapter arrayTransformerWithModelClass:[RepositoriesModel class]];
NSArray *repositories = [transformer transformedValue:response[#"items"]];
return repositories;
});
}
Client:
#pragma mark - fetchRepositoriesByLanguage
- (AnyPromise *)fetchRepositoriesByLanguage:(NSString *)language forPage:(int)page {
NSString *urlString = [NSString stringWithFormat:#"search/repositories?q=language:%#&sort=stars&page=%d", language, page];
return [self fetchWithURLString:urlString].then(^(NSDictionary *response){
return response;
});
}
- (AnyPromise *)fetchWithURLString:(NSString *)stringURL {
return [AnyPromise promiseWithAdapterBlock:^(PMKAdapter _Nonnull adapter) {
NSURL *URL = [NSURL URLWithString:stringURL];
[[APIClient sharedClient] GET:URL.absoluteString parameters:nil progress:nil success:^(NSURLSessionTask *task, id responseObject) {
//NSLog(#"JSON: %#", responseObject);
NSError *error;
adapter(responseObject,error);
} failure:^(NSURLSessionTask *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}];
}
UnitTest:
it(#"should fetchRepositoriesByLanguage not be nil", ^{
id mockHTTPClient = [OCMockObject partialMockForObject:[APIClient sharedClient]];
[[[mockHTTPClient expect] andDo:^(NSInvocation *invocation) {
// we define the sucess block:
void (^thenBlock)(NSDictionary *response) = nil;
// Using NSInvocation, we get access to the concrete block function
// that has been passed in by the actual test
// the arguments for the actual method start with 2 (see NSInvocation doc)
[invocation getArgument:&thenBlock atIndex:1];
// now we invoke the successBlock with some "JSON"...:
thenBlock([NSDictionary dictionaryWithObjectsAndKeys:#"Bom Dia", #"greetings", nil]); //here I got error
}] fetchRepositoriesByLanguage:[OCMArg any] forPage:1];
[mockHTTPClient fetchRepositoriesByLanguage:#"Java" forPage:1].then(^(NSDictionary *response) {
expect(response).toNot.beNil();
});
});
But I always got an error on thenBlock, an EXC_BAD_ACCESS.

How to return a value from a method by delay?

I have a framework and a project. My framework is responsible for web services.
From Project user insert username and password. Then it passes these parameters by calling sendLogin method inside the framework.
Inside framework it takes a while to check and validate username and password. If username and password are correct it will get a token number from server.
Until here everything works fine. But I want to know how to send this token back to main program?
I tried completion method but I failed. Here is definition:
Project:
- (IBAction)bankLoginPressed:(id)sender
{
[registerUser sendLogin:^(NSInteger *accessCode){
NSLog(#"access code == %tu ",accessCode);
}];
}
Inside framework
typedef void (^HttpCompletionBlock) (NSInteger *);
-(void) sendLogin :(HttpCompletionBlock)completionHandler
{
NSString *string = #"https://myserver/customer_authentication";
NSDictionary *parameters = #{#"member_id": #"1234", #"access_code": #"password", #"device_id":#"874627864"};
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager POST:string parameters:parameters progress:nil success:^(NSURLSessionTask *task, id responseObject) {
NSLog(#"JSON: %#", responseObject);
if (responseObject[#"secret_token"])
{
NSLog(#"Secret is= %#",responseObject[#"secret_token"]);
//Here I needd to send back token number????
}
}
failure:^(NSURLSessionTask *operation, NSError *error)
{
NSLog(#"Error: %#", error);
}];
}
typedef void (^HttpCompletionBlock) (NSString *token, NSError *error);
-(void) sendLogin :(HttpCompletionBlock)completionHandler
{
NSString *string = #"https://myserver/customer_authentication";
NSDictionary *parameters = #{#"member_id": #"1234", #"access_code": #"password", #"device_id":#"874627864"};
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager POST:string parameters:parameters progress:nil success:^(NSURLSessionTask *task, id responseObject) {
NSLog(#"JSON: %#", responseObject);
if (responseObject[#"secret_token"])
{
NSLog(#"Secret is= %#",responseObject[#"secret_token"]);
//Here I needd to send back token number????
return completionHandler(responseObject[#"secret_token"],nil);
}
}
failure:^(NSURLSessionTask *operation, NSError *error)
{
NSLog(#"Error: %#", error);
return completionHandler(nil,error);
}];
}
- (IBAction)bankLoginPressed:(id)sender
{
[registerUser sendLogin:^(NSString *token, NSError *error){
if(error == nil)
{
NSLog(#"access code == %# ",token);
}
else
{
NSLog(#"Error == %# ",error);
}
}];
}

Load View Controller even if one API fails

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

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

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.

Resources