Extract Completions Handler Block into method using typdef - objective-c

I would like to clean up my code some and extract a completion block to its own method. It's my understanding that I have to define a typdef with the same signature.
In the below example, I want to move the NSURLSessionUploadTask's on completion handler to its own method. I've looked at the Apple documentation and other SO questions, but I don't know if I am searching for the right thing.
typedef void (^PostCompletionHandler)(NSData *data,NSURLResponse *response,NSError *error);
#interface MyDemoClass() {
#property (copy, nonatomic) PostCompletionHandler completePostHandler;
#end
#implementation MyDemoClass
- (void) post {
NSData *postData = //data to pos;
NSMutableURLRequest * req = //create request;
NSURLSession * session = // create session;
NSURLSessionUploadTask *queryTask = [session uploadTaskWithRequest:req
fromData:postData
completionHandler:^(NSData *data,NSURLResponse *response,NSError *error) {
if (error) {
NSLog(#"Error: %#",error.localizedDescription);
} else {
NSDictionary * response = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
NSLog(#"Post Response: %#",response.description);
}
}];
[queryTask resume];
}
- (void)someMethodThatTakesABlock:(PostCompletionHandler)blockName {
}
#end
This is what I would like to do is something like this:
NSURLSessionUploadTask *queryTask = [session uploadTaskWithRequest:req
fromData:postData
completionHandler:^(NSData *data,NSURLResponse *response,NSError *error) {
// What do I do here?
}];
- (void)someMethodThatTakesABlock:(PostCompletionHandler)blockName {
//how do I access the data, response and error parameters?
if (error) {
NSLog(#"Error: %#",error.localizedDescription);
} else {
NSDictionary * response = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
NSLog(#"Post Response: %#",response.description);
}
}

The someMethodThatTakesABlock: has a single parameter, which is the block. The block is a reference to code that can be executed. It does not contain data.
If you want to separate out your completion handling to a separate method, then write a method
- (void)processCompletionWithData:(NSData*)data response:(NSURLResponse*)response error:(NSError*)error {
// Process it...
}
Then in the completion handler
NSURLSessionUploadTask *queryTask =
[session uploadTaskWithRequest:req
fromData:postData
completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
[self processCompletionWithData:data response:response error:error];
}];

Related

Asynchronously Api calls and returning data outside the block

I don't understand why I am getting null array outside the block code, even though I am using __block keyword on my array.
I am successfully getting data from a backend api with following code
`-(void)getJsonResponse:(NSString *)urlStr success:(void (^)(NSArray *responseDict))success failure:(void(^)(NSError* error))failure
{
NSURLSession *session = [NSURLSession sharedSession];
NSURL *url = [NSURL URLWithString:urlStr];
// Asynchronously API is hit here
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// NSLog(#"%#",data);
if (error)
failure(error);
else {
NSArray *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
// NSLog(#"%#",json);
success(json);
}
}];
[dataTask resume]; // Executed First
}`
Then in my function for returning the data I am using following
`- (NSArray *)get_data:(NSDictionary *)credentials{
NSString *urlStr =[ NSString stringWithFormat:#"http://test.com %#",credentials];
__block NSArray *jsonArray= [[NSArray alloc]init];
[self getJsonResponse:urlStr success:^(NSArray *responseArray) {
jsonArray = responseArray;
NSLog(#"%#",responseArray);
} failure:^(NSError *error) {
// error handling here ...
}];
NSLog(#"%#",jsonArray);
return jsonArray;
}
`
The issue here is although I am successfully getting data within getJsonResponse block, but when I am trying to return the response data array as function return I am getting null for jsonArray. I thought assigning __block infront of jsonArray should retain the data assign within the block code ?
The second approach is not to use Async way like following
`- (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error
{
NSError __block *err = NULL;
NSData __block *data;
BOOL __block reqProcessed = false;
NSURLResponse __block *resp;
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable _data, NSURLResponse * _Nullable _response, NSError * _Nullable _error) {
resp = _response;
err = _error;
data = _data;
reqProcessed = true;
}] resume];
while (!reqProcessed) {
[NSThread sleepForTimeInterval:0];
}
*response = resp;
*error = err;
return data;
}`
That way its blocking the main thread whilst waiting for data.
I would suggest using the same approach of getJsonResponse for your get_data function:
- (void)get_data:(NSDictionary *)credentials finish:(void(^)(NSArray *data))finish{
NSString *urlStr =[ NSString stringWithFormat:#"http://test.com %#",credentials];
__block NSArray *jsonArray= [[NSArray alloc]init];
[self getJsonResponse:urlStr success:^(NSArray *responseArray) {
jsonArray = responseArray;
if (finish) {
finish(jsonArray);
}
} failure:^(NSError *error) {
// error handling here ...
}];
}

Facing an issue with NSURLSessionDataTask with SynchronousRequest in objective-c

Here is my working code with NSURLConnection sendSynchronousRequest :
+ (Inc*)getData:(NSString*)inUUID {
NSString* urlString = [NSString stringWithFormat:#"/inc/%#", incUUID];
NSURLRequest* request = [[HttpRequest requestWithRelativePath:urlString] toNSMutableURLRequest];
NSDictionary* json = [self getJSONForRequest:request];
return [Inc incFromDictionary:json];
}
+ (NSDictionary*)getJSONForRequest:(NSURLRequest*)request {
NSData* responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
return [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];
}
But, sendSynchronousRequest:request is deprecated.
So that, I used NSURLSessionDataTaskinstead of sendSynchronousRequest. Here, is the code which I implemented:
+ (Inc*)getData1:(NSString*)inUUID {
NSString* urlString = [NSString stringWithFormat:#"/in/%#", inUUID];
NSURLRequest* request = [[HttpRequest requestWithRelativePath:urlString] toNSMutableURLRequest];
//NSDictionary* json = [self getJSONForRequest1:request];
__block NSDictionary* json;
dispatch_async(dispatch_get_main_queue(), ^{
[self getJsonResponse1:request success:^(NSDictionary *responseDict) {
json = [responseDict valueForKeyPath:#"detail"];;
//return [Inc incFromDictionary:json];
} failure:^(NSError *error) {
// error handling here ...
}];
});
return [Inc incFromDictionary:json];
}
+ (void)getJsonResponse1:(NSURLRequest *)urlStr success:(void (^)(NSDictionary *responseDict))success failure:(void(^)(NSError* error))failure
{
NSURLSessionDataTask *dataTask1 = [[NSURLSession sharedSession] dataTaskWithRequest:urlStr completionHandler:^(NSData *data, NSURLResponse *response,
NSError *error) {
NSLog(#"%#",data);
if (error)
failure(error);
else {
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#",json);
success(json);
}
}];
[dataTask1 resume]; // Executed First
}
The problem is return statement call in getData1 before finish the api call.
Thanks in advance.
As mentioned in the comments you need a completion handler.
Something like this (untested):
+ (void)getData1:(NSString*)inUUID success:(void (^)(NSDictionary *responseDict))success failure:(void(^)(NSError* error))failure {
NSString* urlString = [NSString stringWithFormat:#"/in/%#", inUUID];
NSURLRequest* request = [[HttpRequest requestWithRelativePath:urlString] toNSMutableURLRequest];
[self getJsonResponse1:request success:^(NSDictionary *responseDict) {
NSDictionary* json = [responseDict valueForKeyPath:#"detail"];
success(json);
} failure:^(NSError *error) {
failure(error);
}];
}
+ (void)getJsonResponse1:(NSURLRequest *)urlStr success:(void (^)(NSDictionary *responseDict))success failure:(void(^)(NSError* error))failure
{
NSURLSessionDataTask *dataTask1 = [[NSURLSession sharedSession] dataTaskWithRequest:urlStr completionHandler:^(NSData *data, NSURLResponse *response,
NSError *error) {
NSLog(#"%#",data);
if (error)
failure(error);
else {
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#",json);
success(json);
}
}];
[dataTask1 resume]; // Executed First
}
And to call
[MyClass getData1:#"asdf" success:^(NSDictionary *responseDict) {
dispatch_async(dispatch_get_main_queue(), ^{
NSDictionary *json = [responseDict valueForKeyPath:#"detail"];
Inc *inc = [Inc incFromDictionary:json];
// do something witrh `inc`
});
} failure:^(NSError *error) {
// error handling here ...
}];
Consider to use instances and instance methods of your class(es) rather than only class methods.

I can't get data with help of NSURLSession

I wrote some test code
-(void)downloadFile
{
[self getFile:^(id result) {
if([result isKindOfClass:[NSData class]])
{
self.myData=result;
}
}];
NSLog(#"%lu",(unsigned long)[self.myData length]);
}
-(void)getFile:(void(^)(id result))completionHandler
{
void(^partCompetionHandle)(NSURL *, NSURLResponse *, NSError *)=^(NSURL *url, NSURLResponse *response, NSError *error){
if(response==nil){
completionHandler(error);
return;
}
NSData *data=[NSData dataWithContentsOfURL:url];
completionHandler(data);
};
self.session=[NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURL *url=[NSURL URLWithString:#"http://upload.wikimedia.org/wikipedia/commons/7/7f/Williams_River-27527.jpg"];
NSURLSessionDownloadTask *dataTask =[_session downloadTaskWithURL:url completionHandler:^(NSURL* data, NSURLResponse * response, NSError * error) {
partCompetionHandle(data, response, error);
}];
[dataTask resume];
}
My datas are zero, when I downloaded theirs
this line shows 0
NSLog(#"%lu",(unsigned long)[self.myData length]);
The classic : As the task is performed asynchronously, the logging of the result is much earlier than the data are downloaded.
Put the log line in the completion block
-(void)downloadFile
{
[self getFile:^(id result) {
if([result isKindOfClass:[NSData class]]) {
self.myData=result;
NSLog(#"%lu",(unsigned long)[self.myData length]);
}
}];
}
I changed code. But I don't see data
-(void)downloadFile
{
[self getFile:^(id result) {
if([result isKindOfClass:[NSData class]])
{
self.myData=result;
NSLog(#"%lu",(unsigned long)[self.myData length]);
}
}];
}
I called method downloadFile from main function
int main(int argc, const char * argv[]) {
#autoreleasepool {
DownloadFile *donload=[DownloadFile new];
[donload downloadFile];
}
return 0;
}
How can I get data?

Best memory management practice for blocks and completion handlers

I have the following code in my app, and there is a memory leak with the 'dict' object. So I have a few questions about the best practice for this after the code:
// Convert JSON to dict
NSError *error = nil;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
dispatch_async(dispatch_get_main_queue(), ^{
// If error return nil
if (error)
completion(nil, [self handleSerializationError:error]);
else if (![UsefulFunctions objectContainsData:dict[#"data"]])
completion(nil, NO);
// No error then return dict
else
completion(dict, NO);
});
About the code: The completion handler passes back the dict object which is then used to create core data entities (hence the main thread) based upon the calling function. The data being serialised is from an NSURLConnection. So the questions are as follows:
1) Is this the correct practice for passing back data in a completion handler?
2) Which function should take care of the memory management, should it be the calling class?
3) Is it worth wrapping this in an auto-release pool, or is that not how they are supposed to be used (kind of a separate question).
4) Can anyone see any obvious reasons from this function alone why the dict is retained, or is it purely down to the calling class?
Thanks for any help
EDIT (Whole Function), just to confirm I am using ARC and this function is called from the main thread:
- (void)downloadJSONFromURL:(NSURL *)url withCompletionHandler:(void (^)(id object, BOOL retry))completion
{
// Check URL
if (url)
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSString *authToken = [NSString stringWithFormat:#"token %#", [UsefulFunctions returnActiveAPIKey]];
[request setValue:authToken forHTTPHeaderField:#"Authorization"];
[request setHTTPMethod:#"GET"];
// Create an asynch request, don't want to hold up main queue
[NSURLConnection sendAsynchronousRequest:request queue:[self operationQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
// If data exists
if (data)
{
// Convert JSON to dict
NSError *error = nil;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
dispatch_async(dispatch_get_main_queue(), ^{
// If error return nil
if (error)
completion(nil, [self handleSerializationError:error]);
else if (![UsefulFunctions objectContainsData:dict[#"data"]])
completion(nil, NO);
// No error then return dict
else
completion(dict, NO);
});
}
// If error
else if (connectionError)
{
//NSLog(#"Connection Error: %#, Code: %lu", connectionError.description, (long)connectionError.code);
dispatch_async(dispatch_get_main_queue(), ^{
// Return nil
completion(nil, [self handleConnectionError:connectionError]);
});
}
}];
}
}

How to show JSON data in UIView labels

About every single tutorial and example on the internet I see shows how to fetch JSON from some url and show it in Tableview. This is not my problem I know how to do that with AFNetworking framework or with native APIs.
My problem is that after I have downloaded the JSON, I want to show some of it in my UIView labels. I have actually succeeded doing this when I was trying to find a way around NSURLSession inability to cache in iOS 8. But I didn't realize that it was synchronous.
Factory.m
+ (Factory *)responseJson
{
static Factory *shared = nil;
shared = [[Factory alloc] init];
NSHTTPURLResponse *response = nil;
NSString *jsonUrlString = [NSString stringWithFormat:#"http://urltojson.com/file.json"];
NSURL *url = [NSURL URLWithString:[jsonUrlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSError *error = nil;
NSURLRequest *request = [NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestReturnCacheDataElseLoad
timeoutInterval:10.0];
NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
if (error) {
NSLog(#"error");
} else {
//-- JSON Parsing
NSDictionary *result = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:nil];
//NSLog(#"Result = %#",result);
shared.responseJson = result;
}
return shared;
}
My question is that is it possible to use for example AFNetwoking to do the same thing? Am I missing some method that I need to call like in case of a TableView
[self.tableView reloadData];
I would like to use that framework because I need to check Reachability and it seems to implement it already.
Edit as asked to show more code
ViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
[self factoryLoad];
[self setupView];
}
- (void)factoryLoad
{
Factory *shared = [Factory responseJson];
self.titles = [shared.responseJson valueForKeyPath:#"data.title"];
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView reloadData];
});
}
- (void)setupView
{
self.issueTitleLabel.text = [self.titles objectAtIndex:0];
}
There are a couple oddities in the code you posted.
Factory, which appears to be a singleton class, should be instantiated inside a dispatch_once to ensure thread safety.
In ViewController.m, you are calling factoryLoad on the main thread, which is subsequently calling sendSynchronousRequest on the main thread. Apple's NSURLConnection Documentation warns against calling this function on the main thread as it blocks the thread, making your application unresponsive to user input.
You should not be passing in nil as the error parameter in NSJSONSerialization JSONObjectWithData:.
In your case I would recommend separating the fetching of data from the construction of your singleton object.
Factory.m
+(Factory *)sharedFactory {
static Factory *sharedFactory = nil;
dispatch_once_t onceToken;
dispatch_once(&onceToken, {
sharedFactory = [[Factory alloc] init];
});
}
-(void)fetchDataInBackgroundWithCompletionHandler:(void(^)(NSURLResponse *response,
NSData *data,
NSError *error)
completion {
NSHTTPURLResponse *response = nil;
NSString *jsonUrlString = [NSString stringWithFormat:#"http://urltojson.com/file.json"];
NSURL *url = [NSURL URLWithString:[jsonUrlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSURLRequest *request = [NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestReturnCacheDataElseLoad
timeoutInterval:10.0];
NSOperationQueue *downloadQueue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request
queue:downloadQueue
completionHandler:completion];
}
Now you should be able to create a reference to the data with a guarantee that the download request has finished and thus the data will exist.
ViewController.m
-(void)factoryLoad {
[[Factory sharedFactory] fetchDataInBackgroundWithCompletionHandler:^(void)(NSURLResponse *response, NSData *data, NSError *error){
if(!error) {
NSError *error2;
NSDictionary *serializedData = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error2];
if(error2){ /* handle error */ }
self.titles = [serializedData valueForKeyPath:#"data.title"];
[Factory sharedFactory].responseJSON = serializedData;
}
else {
// handle error
}
}];
}
This will guarantee that the download has completed before you try to access any of the downloaded information. However, I've left a few things out here, including any sort of activity indicator displaying to the user that the app is doing something important in the background. The rest is, uh, left as an exercise to the reader.
Ok I took a deeper investigation into Morgan Chen's answer and how to block.
The example code took some modification but I think It works as it should and is better code.
In Factory.m
+ (Factory *) sharedInstance
{
static Factory *_sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[self alloc] init];
});
return _sharedInstance;
}
-(void)fetchDataInBackgroundWithCompletionHandler: (void(^)(BOOL success, NSDictionary *data, NSError *error)) block
{
NSString * baseURL = #"http://jsonurl.com/file.json";
AFHTTPRequestOperationManager * manager = [[AFHTTPRequestOperationManager alloc] init];
__weak AFHTTPRequestOperationManager *weakManager = manager;
NSOperationQueue *operationQueue = manager.operationQueue;
[manager.reachabilityManager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
switch (status) {
case AFNetworkReachabilityStatusReachableViaWWAN:
case AFNetworkReachabilityStatusReachableViaWiFi:
NSLog(#"internet!");
[weakManager.requestSerializer setCachePolicy:NSURLRequestReloadIgnoringCacheData];
[operationQueue setSuspended:NO];
break;
case AFNetworkReachabilityStatusNotReachable:
NSLog(#"no internet");
[weakManager.requestSerializer setCachePolicy:NSURLRequestReturnCacheDataElseLoad];
[operationQueue setSuspended:YES];
break;
default:
break;
}
}];
[manager.reachabilityManager startMonitoring];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
[manager GET:baseURL parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (responseObject && [responseObject isKindOfClass:[NSDictionary class]]) {
block(YES, responseObject, nil);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) { // invalid request.
NSLog(#"%#", error.localizedDescription);
block(NO, nil, error);
}];
}
In ViewController.m I call this method on viewDidLoad
-(void)factoryLoad
{
[[Factory sharedInstance] fetchDataInBackgroundWithCompletionHandler:^(BOOL success, NSDictionary *data, NSError *error) {
if (success) {
NSLog(#"we have stuff");
self.responseData = data;
self.titles = [self.responseData valueForKeyPath:#"data.title"];
[self setupView];
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView reloadData];
});
}
}];
}