I am new to blocks. I am trying to analyze how the following code works.
As i understand this specific method has a block and returns a
NSURLSessionDataTask
. getTotalFollowersFrom is the name of the method. (NSString*)influencerId is the userId that is passed when calling this method. WithCompletion: is used so we know when the method completes the Api call. Void is what the block returns. The caret symbol (^) is used to define the block. The following (id _Nullable likesCountRequestResponse, NSError * _Nullable error)completion are arguments. This is what i do not understand. According to documentation arguments have return values inside the block. I see the return result; that returns the NSURLSessionDataTask but i do not understand how the values return for the arguments of the block. What am i missing? Could anyone please explain to me?
- (NSURLSessionDataTask *)getTotalLikesFrom:(NSString*)userId withCompletion:(void (^)(id _Nullable likesCountRequestResponse, NSError * _Nullable error))completion {
NSString* postString = [NSString stringWithFormat:#"apicall/%#", userId];
#weakify(self)
NSURLSessionDataTask *result = [self GET:postString parameters:nil completion:^(OVCResponse * _Nullable response, NSError * _Nullable error) {
#strongify(self)
[self handleResponse:response error:error adjustErrorBlock:self.commonValidationErrorAdjustBlock completion:completion];
}];
return result;
}
- (void)handleResponse:(nullable OVCResponse *)response error:(nullable NSError *)error adjustErrorBlock:(nullable ApiClientv2AdjustErrorBlock)adjustErrorBlock completion:(void (^)(id _Nullable result, NSError * _Nullable error))completion
{
if (response.HTTPResponse.statusCode >= 500)
{
error = nil != error ? error : NSError.com_eight_APIServiceUnknownError; [SentrySDK captureError:error];
}
else
{
error = nil != error ? error : NSError.com_eight_APIServiceUnknownError;
}
id result = nil == error ? response.result : nil;
completion(result, error);
}
Your example is not complete. So, let's consider a MCVE:
- (NSURLSessionDataTask *)networkRequestWithCompletion:(void (^)(NSDictionary * _Nullable object, NSError * _Nullable error))completion {
NSURL *url = [NSURL URLWithString:#"https://httpbin.org/get"];
NSURLSessionDataTask *task = [NSURLSession.sharedSession dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!data || error) {
completion(nil, error);
return;
}
NSError *parseError;
NSDictionary *resultObject = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&parseError];
if (parseError) {
completion(nil, parseError);
return;
}
completion(resultObject, nil);
}];
[task resume];
return task;
}
This performs a network request and parses the JSON. Inside that method, you will see references to:
completion(nil, error);
Or:
completion(resultObject, nil);
That is how the data is passed back to the caller. The method supplies the parameters of the block when it calls completion . Thus, this method can be supplied a block and use these two parameters:
[self networkRequestWithCompletion:^(NSDictionary *dictionary, NSError *error) {
if (error) {
NSLog(#"error = %#", error);
return;
}
// you can access the `dictionary` parameter here ...
NSLog(#"dictionary = %#", dictionary);
}];
// ... but you cannot reference the `dictionary` or `error` parameters here
// because the above runs asynchronously (i.e., later).
In the code snippet you supplied in your question, you are not calling completion anywhere. But notice that it is supplied to handleResponse. That method is undoubtedly calling the block for you.
As a matter of personal preference, I think that the choice of result for the NSURLSessionDataTask is a little confusing. So in my example, I gave it a better name, task, to make it clear that it is the NSURLSessionTask object, not the result of the network request.
But what is the purpose of this object? It is so that the caller has the option to cancel the request if it wants. For example, you might have:
#interface ViewController ()
#property (nonatomic, weak) NSURLSessionTask *task;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.task = [self networkRequestWithCompletion:^(NSDictionary *dictionary, NSError *error) {
if (error) {
NSLog(#"error = %#", error);
return;
}
NSLog(#"dictionary = %#", dictionary);
}];
}
- (IBAction)didTapCancelButton:(id)sender {
[self.task cancel];
}
...
#end
So, the NSURLSessionTask reference is returned, which we save, so that we can have, for example, a cancel button that will cancel that asynchronous network request.
But, in short, do not conflate the NSURLSessionTask reference that is immediately returned by networkRequestWithCompletion (to give us the option to cancel the request later) with the parameters of the block, which we use to supply the caller with the results of the network request.
Related
Here is where I call the class method. The call is made after a NSURLRequest is finished. All values are there, nothing is nil
[MemberInfo SetMemberInfo:memberId groupId:groupId token:token withContext:_context];
Here is the method implemented in the class generated by the core data "MemberInfo+CoreDataProperties.m"
+ (bool)SetMemberInfo:(NSString *)memberId groupId:(NSString *)groupId token:(NSString *)token withContext:(NSManagedObjectContext *)context
{
NSManagedObject *memberInfoObject = [NSEntityDescription insertNewObjectForEntityForName:#"MemberInfo" inManagedObjectContext:context];
[memberInfoObject setValue:memberId forKey:#"memberId"];
[memberInfoObject setValue:groupId forKey:#"groupId"];
[memberInfoObject setValue:token forKey:#"token"];
NSError *error = nil;
if (![context save:&error])
{
return false;
}
return true;
}
I have zero errors, and nothing in the logs that explains why. But this class method 'SetMemberInfo' is never hit. Any clues?
EDIT **
Full code where I call method
NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error == nil)
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if ([httpResponse statusCode] == 200)
{
id object = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
if ([object isKindOfClass:[NSDictionary class]] && error == nil)
{
NSString *groupId = _tfGroupId.text;
NSString *memberId = _tfMemberId.text;
NSString *token = [object valueForKey:#"token"];
[MemberInfo SetMemberInfo:memberId groupId:groupId token:token withContext:_context];
}
}
}
}];
[postDataTask resume];
Must be something to do with the class that I has the class method in. I moved it to another class and it now makes the call.
I'm using the code below to verify that my Shopify check out was successful... However the line:
[self.client getCompletionStatusOfCheckout:self.checkout
completion:^(BUYCheckout *checkout, BUYStatus status, NSError *error)
{
is throwing me the strangest error:
"Incompatible block pointer types sending 'void(^)(BUYCheckout
*___Strong, BUYStatus, NSError *___Strong' to parameter type 'BUYDataCheckoutStatusBlock''
How can I fix this? And what does it mean? Let me know if you need more code/details. Cheers.
.h
#property (nonatomic, strong) BUYClient *client;
.m
-(void)verifyCheckout{
__block BUYStatus buyStatus = BUYStatusUnknown;
__block BUYCheckout *completedCheckout = self.checkout;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
do {
[self.client getCompletionStatusOfCheckout:self.checkout completion:^(BUYCheckout *checkout, BUYStatus status, NSError *error) {
completedCheckout = checkout;
buyStatus = status;
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (buyStatus == BUYStatusProcessing) {
[NSThread sleepForTimeInterval:0.5];
} else {
}
} while (completedCheckout.token && buyStatus != BUYStatusFailed && buyStatus != BUYStatusComplete);
}
The Shopify documentation says that the completion block takes in only two parameters:
typedef void (^BUYDataCheckoutStatusBlock)(BUYStatus status, NSError *error);
So the line
[self.client getCompletionStatusOfCheckout:self.checkout
completion:^(BUYCheckout *checkout, BUYStatus status, NSError *error)
{
Should instead be
[self.client getCompletionStatusOfCheckout:self.checkout
completion:^(BUYStatus status, NSError *error)
{
How can I wait for finish of dataTaskWithRequest ? I need to perform some tasks after network fetch is completely over.
If you really need synchronous request, you can use semaphores.
I've implemented a small category on NSURLSession to provide this functionality.
In .h file:
#import Foundation.NSURLSession;
#interface NSURLSession (Additions)
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error;
#end
In .m file:
#implementation NSURLSession (Additions)
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse *__autoreleasing *)responsePointer error:(NSError *__autoreleasing *)errorPointer
{
dispatch_semaphore_t semaphore;
__block NSData *result = nil;
semaphore = dispatch_semaphore_create(0);
void (^completionHandler)(NSData * __nullable data, NSURLResponse * __nullable response, NSError * __nullable error);
completionHandler = ^(NSData * __nullable data, NSURLResponse * __nullable response, NSError * __nullable error)
{
if ( errorPointer != NULL )
{
*errorPointer = error;
}
if ( responsePointer != NULL )
{
*responsePointer = response;
}
if ( error == nil )
{
result = data;
}
dispatch_semaphore_signal(semaphore);
};
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:completionHandler] resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return result;
}
#end
I think, that method is obvious inside the class
NSURLSessionDataTask *task = [defaultSession dataTaskWithRequest:request
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error)
{
// code after completion of task
}];
[task resume];
- (void) loginRequest:(NSString*) username withPassword:(NSString *) password callback:(void (^)(NSError *error, BOOL success))callback
{
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
if (error) {
// Handle error, optionally using
callback(error, NO);
}
else {
callback(nil, YES);
}
}];
[dataTask resume];
}
Call this method like so:
[self loginRequest:#"myUsername" password:#"password" callback:^(NSError *error, BOOL success) {
if (success) {
NSLog(#"My response back from the server after an unknown amount of time");
}
else {
NSLog(#"%#", error);
}
}];
lets just do your tasks in the completion block of dataTaskWithRequest.
Until than you can display an activity indicator to block the user from touching anything on the screen.
Example:
activityIndicator.startAnimating()
let task : NSURLSessionDataTask = session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in
activityIndicator.stopAnimating()
// do your stuff here
});
Recently I started developing for iOS and faced problem which is maybe obvious for you but I couldn't figure it out by myself.
What I'm trying to do is to execute task after another one, using multithreading provided by GCD.
This is my code for fetching JSON (put in class with singleton)
CategoriesStore
- (instancetype)initPrivate {
self = [super init];
if (self) {
[self sessionConf];
NSURLSessionDataTask *getCategories =
[self.session dataTaskWithURL:categoriesURL
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
if (error) {
NSLog(#"error - %#",error.localizedDescription);
}
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse *) response;
if (httpResp.statusCode == 200) {
NSError *jsonError;
NSArray *json =
[NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableContainers
error:&jsonError];
if (!jsonError) {
_allCategories = json;
NSLog(#"allcategories - %#",_allCategories);
}
}
}];
[getCategories resume];
}
return self;
}
Then in ViewController I execute
- (void)fetchCategories {
NSLog(#"before");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
CategoriesStore *categories = [CategoriesStore sharedStore];
dispatch_async(dispatch_get_main_queue(), ^(void) {
_allDirectories = categories.allCategories;
[self.tableView reloadData];
NSLog(#"after");
});
});
}
-fetchCategories is executed in viewDidAppear. The result is usually before, after and then JSON. Obviously what I want to get is before, json after.
I also tried to do this with dispatch_group_notify but didn't workd.
How can I get it working? Why it doesn't wait for first task to be finished?
Thank's for any help!
Regards, Adrian.
I would suggest to define a dedicated method in CategoriesStore that fetches data from remote server and takes callback as an argument:
- (void)fetchDataWithCallback:(void(^)(NSArray *allCategories, NSError* error))callback
{
NSURLSessionDataTask *getCategories =
[self.session dataTaskWithURL:categoriesURL
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
if (error) {
NSLog(#"error - %#",error.localizedDescription);
callback(nil, error);
return;
}
NSError *jsonError = nil;
NSArray *json =
[NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableContainers
error:&jsonError];
if (!jsonError) {
_allCategories = json;
NSLog(#"allcategories - %#",_allCategories);
callback(_allCategories, nil);
} else {
callback(nil, jsonError);
}
}];
[getCategories resume];
}
And you can use it in your ViewController:
- (void)fetchCategories {
[[CategoriesStore sharedStore] fetchDataWithCallback:^(NSArray *allCategories, NSError* error) {
if (error) {
// handle error here
} else {
_allDirectories = allCategories;
[self.tableView reloadData];
}
}]
}
In this way you will reload your table view after data loading & parsing.
You have to wait for the reload data so you may do something like this, another option if you don't want to wait for the whole block and just for the fetch is to use a custom NSLock
dispatch_sync(dispatch_get_main_queue(), {
_allDirectories = categories.allCategories;
[self.tableView reloadData];
}
NSLog(#"after");
I used method suggested by #sgl0v, although it wasn't solution I expected.
Another way to do this is by using notification center and listening for event to occur.
I'm using AFNetworking and I am overridding -dataTaskWithRequest:completionHandler: to basically MITM my request and do some error handling prior to calling the actual response block. However, I need access to the NSURLSessionDataTask object that the -dataTaskWithRequest:completionHandler: method creates, inside my intercepted completion handler. So my override of the method looks like this:
-(NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLResponse *, id, NSError *))completionHandler {
void (^interceptedCompletionHandler)(NSURLResponse *, id, NSError *) = ^void(NSURLResponse * response, id responseObject, NSError * error) {
if (error) {
// Do custom stuff here that needs to use task.taskIdentifier
}
// Then call the original completion handler
completionHandler(response, responseObject, error);
}
return [super dataTaskWithRequest:request completionHandler:interceptedCompletionHandler];
}
Is this possible?
I know that AFNetworking could add this quite easily as the response parameter of the completion handler is set as task.response in AFURLSessionManager, which is the class that contains the -URLSession:task:didCompleteWithError: that calls the completion handler.
I've figured it out. By declaring the block inline and assigning the super call to a variable, I'm able to use the resulting task object:
-(NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLResponse *, id, NSError *))completionHandler {
__block NSURLSessionDataTask* task = [super dataTaskWithRequest:request completionHandler:
^void(NSURLResponse * response, id responseObject, NSError * error)
{
// I can use task here
if (error) {
// Do custom stuff here that needs to use task.taskIdentifier
}
// Then call the original completion handler
completionHandler(response, responseObject, error);
}
return task;
}