Problem-> i just wanna fetch scores of friends not on main queue but on different queue and in that queue I also called login, openSession methods for sign in. I wanna try to fetch scores
STEP 1 -> -(void)requestcallerWithQueue in this method i m trying to fetch score inside queue created by me (not main queue) then FBRequestConnection startWithGraphPath: method call in FBRequestConnection startWithGraphPath: NOT RESPONDING.
STEP 2 -> -(void)requestcallerWithoutQueue in this method i m trying to fetch score inside queue created by me (not main queue) then FBRequestConnection startWithGraphPath: method call in FBRequestConnection startWithGraphPath: RESPONDING
**CODE IS BELOW FOR REFERENCE**
-(void)fetchscoreWithCallback:(void (^)(NSDictionary*))callback
{
NSMutableDictionary* params = [NSMutableDictionary dictionaryWithObjectsAndKeys:
[NSString stringWithFormat:#"%ld", (long)10], #"score",
nil];
// [NSString stringWithFormat:#"%llu/scores", lintFBFriendId]
[FBRequestConnection startWithGraphPath:#"245526345633359/scores"
parameters:params
HTTPMethod:#"GET"
completionHandler:^(FBRequestConnection *connection, id result, NSError *error)
{
NSLog(#"result->%#",result);
}
}
NOT WORKING
-(void)requestcallerWithQueue
{
dispatch_queue_t some_queue = dispatch_queue_create("some.queue.name", NULL);
dispatch_async(some_queue,
^{
[self fetchscoreWithCallback:callback];
}];
}
WORKING
-(void)requestcallerWithoutQueue
{
[self fetchscoreWithCallback:callback];
}
FBRequestConnection uses NSURLConnection which by default requires the calling thread to have a run loop. For non main threads, you have to manage the run loop yourself or dispatch the work back to the main queue. For some resources, check out:
http://coc24hours.blogspot.com/2012/01/using-nsrunloop-and-nsurlconnection.html
NSURLConnection needs a NSRunLoop to execute?
http://iosdevelopmentjournal.com/blog/2013/01/27/running-network-requests-in-the-background/
Related
I am using a block from Facebook SDK. It returns a dictionary. I want that dictionary as a return value of a method. I am trying to wrap my head around the whole block concept but need a nudge in the right direction.
The block:
(the argument for the block is a string userFBid)
-(NSDictionary*) getMutualFBFriendsWithFBid:(NSString*)fbID {
[FBRequestConnection startWithGraphPath:[NSString stringWithFormat:#"/%#/mutualfriends/%#", [[PFUser currentUser] objectForKey:kFbID],userFBid]
parameters:nil
HTTPMethod:#"GET"
completionHandler:^(
FBRequestConnection *connection,
id result,
NSError *error
) {
result = (NSDictionary*)result;
//return result;
}];
}
How do i get the return value?
I have tried to google it, but i cant get my hand around it.
I would appreciate any pointer in the right direction.
EDIT:
The main question is the following: I need to the completion handler to call a method in another class... how to do that?
As the method startWithGraphPath is asynchronous, you can't code as if it was synchronous : it means no return value, because as soon as this method is called, your app will continue to execute to the next line, and won't wait for a returned value.
So, to keep this async, I assume you want to use the result of this in your own function, so call it in your completionHandler block:
[FBRequestConnection startWithGraphPath:[NSString stringWithFormat:#"/%#/mutualfriends/%#", [[PFUser currentUser] objectForKey:kFbID],userFBid]
parameters:nil
HTTPMethod:#"GET"
completionHandler:^(FBRequestConnection *connection, id result, NSError *error) {
[self myRockinFunction:result];
}];
//Considering this function
-(void)myRockinFunction:(NSDictionary*) fb_result{
//Do stuff with fb_result
}
Edit
OK, I get it. Modify your method to accept a callback as a parameter :
-(NSDictionary*) getMutualFBFriendsWithFBid:(NSString*)fbID andCallback:(void (^)(NSDictionary *))callback {
[FBRequestConnection startWithGraphPath:[NSString stringWithFormat:#"/%#/mutualfriends/%#", [[PFUser currentUser] objectForKey:kFbID],userFBid]
parameters:nil
HTTPMethod:#"GET"
completionHandler:^(FBRequestConnection *connection,id result,NSError *error) {
//You should treat errors first
//Then cast the result to an NSDictionary
callback((NSDictionary*) result); //And trigger the callback with the result
}];
}
Then, in your other class, use another block to treat your result :
[YourHelperClass getMutualFBFriendsWithFBid:fbID andCallback:^(NSDictionary* result){
//Use result how you wish
//Beware, this is async too.
}];
Note : you should treat the error before triggering your callback.
Edit 2 (Help from other users appreciated)
Even better, you might try to pass a callback taking all the parameters (not tested, and not sure of the syntax. If someone can correct me, I'd appreciate):
-(NSDictionary*) getMutualFBFriendsWithFBid:(NSString*)fbID andCallback:(void (^)(FBRequestConnection *,NSDictionary *,NSError *))callback {
[FBRequestConnection startWithGraphPath:[NSString stringWithFormat:#"/%#/mutualfriends/%#", [[PFUser currentUser] objectForKey:kFbID],userFBid]
parameters:nil
HTTPMethod:#"GET"
completionHandler:callback()]; //Not sure here!
}
[YourHelperClass getMutualFBFriendsWithFBid:fbID andCallback:^(FBRequestConnection *connection,NSDictionary * result,NSError *error){
//You could deal with errors here now
}];
Here's a reference on Apple's docs for deeper understanding.
You already have it :)
I would write a method to process the dictionary, in order to keep the completionHandler block a little cleaner--but you could write your response-handling code inside the block. As another commenter mentioned, this is async so you're not really "returning" anything...you're handling the completion block when it gets called.
To help you understand a little, the completionHandler block in this case is a chunk of code that you're passing to the method as an argument, for it to call later. In essence, "whenever this call comes back, do this: ^{ ". The implementation of the FBRequest method will call your completionHandler (whatever that may be).
I have a unit test in which I need to wait for an async task to finish. I am trying to use NSConditionLock as it seems to be a pretty clean solution but I cannot get it to work.
Some test code:
- (void)testSuccess
{
loginLock = [[NSConditionLock alloc] init];
Login login = [[Login alloc] init];
login.delegate = self;
// The login method will make an async call.
// I have setup myself as the delegate.
// I would like to wait to the delegate method to get called
// before my test finishes
[login login];
// try to lock to wait for delegate to get called
[loginLock lockWhenCondition:1];
// At this point I can do some verification
NSLog(#"Done running login test");
}
// delegate method that gets called after login success
- (void) loginSuccess {
NSLog(#"login success");
// Cool the delegate was called this should let the test continue
[loginLock unlockWithCondition:1];
}
I was trying to follow the solution here:
How to unit test asynchronous APIs?
My delegate never gets called if I lock. If I take out the lock code and put in a simple timer it works fine.
Am I locking the entire thread and not letting the login code run and actually make the async call?
I also tried this to put the login call on a different thread so it does not get locked.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[login login];
});
What am I doing wrong?
EDIT adding login code. Trimmed do the code for readability sake. Basically just use AFNetworking to execute a POST. When done will call delegate methods.
Login make a http request:
NSString *url = [NSString stringWithFormat:#"%#/%#", [_baseURL absoluteString], #"api/login"];
[manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (_delegate) {
[_delegate loginSuccess];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (_delegate) {
[_delegate loginFailure];
}
}];
The answer can be found in https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFHTTPRequestOperation.m.
Since you are not setting the completionQueue property of the implicitly created AFHTTPRequestOperation, it is scheduling the callbacks on the main queue, which you are blocking.
Unfortunately, many answers (not all) in the given SO thread ("How to unit test asynchronous APIs?") are bogus and contain subtle issues. Most authors don't care about thread-safity, the need for memory-barriers when accessing shared variables, and how run loops do work actually. In effect, this leads to unreliable and ineffective code.
In your example, the culprit is likely, that your delegate methods are dispatched on the main thread. Since you are waiting on the condition lock on the main thread as well, this leads to a dead lock. One thing, the most accepted answer that suggests this solution does not mention at all.
A possible solution:
First, change your login method so that it has a proper completion handler parameter, which a call-site can set in order to figure that the login process is complete:
typedef void (^void)(completion_t)(id result, NSError* error);
- (void) loginWithCompletion:(completion_t)completion;
After your Edit:
You could implement your login method as follows:
- (void) loginWithCompletion:(completion_t)completion
{
NSString *url = [NSString stringWithFormat:#"%#/%#", [_baseURL absoluteString], #"api/login"];
[manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (completion) {
completion(responseObject, nil);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (completion) {
completion(nil, error);
}
}];
Possible usage:
[self loginWithCompletion:^(id result, NSError* error){
if (error) {
[_delegate loginFailure:error];
}
else {
// Login succeeded with "result"
[_delegate loginSuccess];
}
}];
Now, you have an actual method which you can test. Not actually sure WHAT you are trying to test, but for example:
-(void) testLoginController {
// setup Network MOCK and/or loginController so that it fails:
...
[loginController loginWithCompletion:^(id result, NSError*error){
XCTAssertNotNil(error, #"");
XCTAssert(...);
<signal completion>
}];
<wait on the run loop until completion>
// Test possible side effects:
XCTAssert(loginController.isLoggedIn == NO, #""):
}
For any other further steps, this may help:
If you don't mind to utilize a third party framework, you can then implement the <signal completion> and <wait on the run loop until completion> tasks and other things as described here in this answer: Unit testing Parse framework iOS
My assumption is that the operations are running asynchronously on a separate thread, but the loop never exits, so something is not as I assumed.
/**
Checks if we can communicate with the APIs
#result YES if the network is available and all of the registered APIs are responsive
*/
- (BOOL)apisAvailable
{
// Check network reachability
if (!_connectionAvailable) {
return NO;
}
// Check API server response
NSMutableSet *activeOperations = [[NSMutableSet alloc] init];
__block NSInteger successfulRequests = 0;
__block NSInteger failedRequests = 0;
for (AFHTTPClient *httpClient in _httpClients) {
// Send heart beat request
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:#"" parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// Server returned good response
successfulRequests += 1;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Server returned bad response
failedRequests += 1;
}];
[operation start];
[activeOperations addObject:operation];
}
// Wait for heart beat requests to finish
while (_httpClients.count > (successfulRequests + failedRequests)) {
// Wait for each operation to finish, one at a time
//usleep(150);
[NSThread sleepForTimeInterval:0.150];
}
// Check final results
if (failedRequests > 0) {
return NO;
}
return YES;
}
A few suggestions:
Never check reachability to determine if a request will succeed. You should try the request; only if it fails should you consult reachability to try and get a best guess as to why. Reachability makes no guarantee about whether a request will fail or succeed.
Is this method called on the main thread? Even if you fixed the problem with the requests never completing, it will block the UI the entire time your network requests are running. Since these requests can take potentially a long time, this is a bad experience for the user as well as something the OS will kill your app for if it happens at the wrong time (e.g. at launch).
Looping while calling sleep or equivalent is wasteful of CPU resources and memory, as well as prevents the thread's runloop from servicing any timers, event handler or callbacks. (Which is probably why the networking completion blocks never get to run.) If you can avoid blocking a thread, you should. In addition, Cocoa will very often be unhappy if you do this on an NSThread you didn't create yourself.
I see two options:
Use dispatch_groups to wait for all of your requests to finish. Instead of blocking your calling thread, you should instead take a completion block to call when you're done. So, instead of returning a BOOL, take a completion block which takes a BOOL. Something like - (void)determineIfAPIIsAvailable:(void(^)(BOOL))completionBlock;
Get rid of this method altogether. What are you using this method for? It's almost certainly a better idea to just try to use your API and report appropriate errors to the user when things fail rather than to try to guess if a request to the API will succeed beforehand.
I believe the issue is that I was not using locking to increment the counters so the while loop would never evaluate to true.
I was able to get it working by only looking for a fail count greater than 0 that way as long as it was incremented by any of the request callback blocks then I know what to do.
I just so happen to have switched to [NSOperationQueue waitUntilAllOperationsAreFinished].
Final code:
/**
Checks if we can communicate with the APIs
#result YES if the network is available and all of the registered APIs are responsive
*/
- (BOOL)apisAvailable
{
// Check network reachability
if (!_connectionAvailable) {
return NO;
}
// Check API server response
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
__block NSInteger failedRequests = 0;
for (AFHTTPClient *httpClient in _httpClients) {
// Send heart beat request
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:#"" parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// Server returned good response
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Server returned bad response
failedRequests += 1;
}];
[operationQueue addOperation:operation];
}
// Wait for heart beat requests to finish
[operationQueue waitUntilAllOperationsAreFinished];
// Check final results
if (failedRequests > 0) {
return NO;
}
return YES;
}
I've integrated with Facebook so that I can, among other things, post statuses to my feed. I based some of my code off of the publish to feed developer tutorial. When running the following Graph API request from my iOS application the completion block of the request is never called and no error appears in the XCode debug log.
[FBRequestConnection
startWithGraphPath:#"me/feed"
parameters:params
HTTPMethod:#"POST"
completionHandler:^(FBRequestConnection *connection, id result, NSError *error) {
if (error) {
DLog(#"error: domain = %#, code = %d", error.domain, error.code);
} else {
DLog(#"Posted action, id: %#", result[#"id"]);
}
}];
I have two convenience functions that perform checks against the current activeSession before executing this request. They look like this:
+ (BOOL)facebookSessionIsOpen {
return (FBSession.activeSession.isOpen);
}
+ (BOOL)facebookSessionHasPublishPermissions {
if ([FBSession.activeSession.permissions indexOfObject:#"publish_actions"] == NSNotFound ||
[FBSession.activeSession.permissions indexOfObject:#"publish_stream"] == NSNotFound ||
[FBSession.activeSession.permissions indexOfObject:#"manage_friendlists"] == NSNotFound) {
return NO;
} else {
return YES;
}
}
Both of these functions return YES indicating an active session with the necessary publishing permission. What's more confusing is that I can pull the user's profile without issue after performing the same checks successfully (granted publishing permissions are not required to pull the profile) using the following code:
[FBRequestConnection
startWithGraphPath:#"me"
parameters:[NSDictionary dictionaryWithObject:#"picture,id,birthday,email,location,hometown" forKey:#"fields"]
HTTPMethod:#"GET"
completionHandler:^(FBRequestConnection *connection, id result, NSError *error) {
NSDictionary* resultDict = (NSDictionary*)result;
NSString* emailAddress = resultDict[#"email"];
NSString* location = resultDict[#"location"][#"name"];
NSString* birthday = resultDict[#"birthday"];
NSString* homeTown = resultDict[#"hometown"][#"name"];
...
}];
Any suggestions on how to debug this issue?
Turns out the issue was a threading one. The Facebook iOS SDK doesn't seem to like to execute a FBRequest on a different thread from the one that you called openActiveSessionWithReadPermissions on and promptly deadlocks. It turns out I was running the postStatus request in a separate thread like so:
dispatch_queue_t some_queue = dispatch_queue_create("some.queue.name", NULL);
dispatch_async(some_queue, ^{
[FacebookHelper postStatusToFacebookUserWall:newStatus withImage:imageData];
}];
Make sure your openActiveSessionWithReadPermissions and any FBRequest permutations all happen on the same thread, or you'll likely run into these silent failures.
I am attempting to load a collection of objects via RestKit and have found that this operation is blocking the main thread. Specifically, this functionality is called when a UIViewController appears in the -(void)viewDidAppear:(BOOL)animated; message. As long as request is loading, the user is unable to interact with the UI, and has to wait until the operation has completed.
How can I instruct RestKit to execute the request asynchronously (I thought it already was) and stop blocking the main thread?
-(void)rtrvArtistsByStudio:(ISStudio *)studio
{
NSLog(#"Retrieving Artists for studio %ld", [studio studioID]);
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
NSDictionary *params = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:#"%ld", [studio studioID]] forKey:#"studioID"];
NSURLRequest *request = [restkit requestWithObject:nil method:RKRequestMethodGET path:#"/restAPI/rtrvArtistsByStudio" parameters:params];
RKManagedObjectRequestOperation *operation = [restkit managedObjectRequestOperationWithRequest:request managedObjectContext:restkit.managedObjectStore.persistentStoreManagedObjectContext success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
NSArray *artists = [mappingResult array];
[studio setArtists:artists];
[notificationCenter postNotificationName:nStudioLoadedArtists object:studio];
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"Failed to load artists for studio %ld due to %#", [studio studioID], error);
[notificationCenter postNotificationName:nStudioFailedLoadingArtists object:studio];
}];
[restkit enqueueObjectRequestOperation:operation];
}
EDIT:
I should also note that restkit is a pointer to an instance of RKObjectManager.
RestKit version is 0.20
The request is definitely being dispatched asynchronously. If you press the pause button in the Xcode UI and look at the threads, what is happening on the main thread and the background threads during the request? That should provide insight into what is causing your thread blockage.
Turns out that it was actually my own fault (of course). I have a UIViewController category that performs some transitions. During the transition I have the UIApplication ignore touch events until the transition has completed (0.2s, very brief). I completely forgot that this existed.
Because I am loading some remote objects in viewDidAppear it was still ignoring touch events until that completed. Removing the touch ignore fixed the problem.