In an iOS app, I'm running a fairly large script on a UIWebView using stringByEvaluatingJavaScriptFromString (large in terms of the length of the javascript string). There is a brief pause after calling the javascript causing other elements on the screen to hiccup for a moment.
Placing the javascript call in a function called in the background with self performSelectorInBackground breaks the application. Is there a safe way to call run this on a background thread or otherwise prevent the interface from pausing?
No, Webviews and the Webkit JavaScript engine are both single-threaded and cannot be used on a background thread.
A better option is to split up your JavaScript into discrete execution blocks and pipeline them using a JavaScript timer, like this (JS code, not Obj-C):
var i = 0;
var operation = function() {
switch (i) {
case 0:
//do first part of code
break;
case 1:
//do second part of code
break;
case 2:
//do third part of code
break;
etc...
}
//prepare to execute next block
i++;
if (i < TOTAL_PARTS) {
setTimeout(operation, 0);
}
};
operation();
That will prevent your script from blocking user interaction while it executes
Well, I was doing the same thing. I had to run a synchronous ajax request which was freezing my UI. So this is how I fixed it :
__block NSString *message;
dispatch_queue_t q = dispatch_queue_create("sign up Q", NULL);
dispatch_async(q, ^{
NSString *function = [[NSString alloc] initWithFormat: #"signup(\'%#\',\'%#\',\'%#\')",self.email.text,self.password.text,self.name.text];
dispatch_async(dispatch_get_main_queue(), ^{
NSString *result = [self.webView stringByEvaluatingJavaScriptFromString:function];
NSLog(#"%#",result);
if ([result isEqualToString:#"1"]) {
message = [NSString stringWithFormat:#"Welcome %#",self.name.text];
[self.activityIndicator stopAnimating];
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}
else {
message = [NSString stringWithFormat:#"%# is a registered user",self.name.text];
[self.activityIndicator stopAnimating];
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Message" message:message delegate:self cancelButtonTitle:#"Okay" otherButtonTitles: nil];
[alertView show];
});
});
The logic is simple. Go to a new thread, and from within that, dispatch to the main queue and then do the JS work and everything worked like a charm for me...
Anything you do with a UIWebView must be done on the main thread. It's a UI element, so this is why performSelectorInBackground breaks your app.
You could try putting that call into an NSOperation. Since you are working with a UI element, be sure to use the [NSOperationQueue mainQueue].
Related
I am looking at the Ray Wenderlich tutorial on using dispatch queues to get notified when a group of tasks complete. http://www.raywenderlich.com/63338/grand-central-dispatch-in-depth-part-2
The first code shown under "Code that works" is straight from the tutorial. The Alert view(final completion block) get executed after all 3 downloads complete.
I tried to play around with it and moved the dispatch async down in the "Code that does not work" to see what will happen if dispatch_group_create() and dispatch_group_enter() happen on different queues. In this case, the dispatch_group_enter() does not seem to register because the dispatch_group_wait() immediately completes and alert view(final completion block) is executed even before all the downloads have completed.
Can someone explain whats happening in the second case? (This is just for my understanding of how dispatch group works and I realize thats its better to put the entire function in the global concurrent queue to avoid blocking the main thread).
Code that works
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),^{
__block NSError *error;
dispatch_group_t downloadGroup = dispatch_group_create();
for (NSInteger i = 0; i < 3; i++)
{
NSURL *url;
switch (i) {
case 0:
url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
break;
case 1:
url = [NSURL URLWithString:kSuccessKidURLString];
break;
case 2:
url = [NSURL URLWithString:kLotsOfFacesURLString];
break;
default:
break;
}
dispatch_group_enter(downloadGroup);
__block Photo *photo = [[Photo alloc] initwithURL:url
withCompletionBlock:^(UIImage *image, NSError *_error) {
if (_error) {
error = _error;
}
NSLog(#"Finished completion block for photo alloc for URL %# and photo is %#",url,photo) ;
dispatch_group_leave(downloadGroup);
}];
[[PhotoManager sharedManager] addPhoto:photo];
NSLog(#"Finished adding photo to shared manager for URL %# and photo is %#",url,photo) ;
}
dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5
dispatch_async(dispatch_get_main_queue(), ^{
if (completionBlock) {
NSLog(#"Executing completion block after download group complete") ;
completionBlock(error);
}
}) ;
}) ;
}
EDITED Code that does not work with extra NSLog statements
Code that does not work
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
__block NSError *error;
dispatch_group_t downloadGroup = dispatch_group_create();
for (NSInteger i = 0; i < 3; i++)
{
NSURL *url;
switch (i) {
case 0:
url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
break;
case 1:
url = [NSURL URLWithString:kSuccessKidURLString];
break;
case 2:
url = [NSURL URLWithString:kLotsOfFacesURLString];
break;
default:
break;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),^{
dispatch_group_enter(downloadGroup);
NSLog(#"Enetered group for URL %#",url) ;
__block Photo *photo = [[Photo alloc] initwithURL:url
withCompletionBlock:^(UIImage *image, NSError *_error) {
if (_error) {
error = _error;
}
NSLog(#"Finished completion block for photo alloc for URL %# and photo is %#",url,photo) ;
dispatch_group_leave(downloadGroup);
}];
[[PhotoManager sharedManager] addPhoto:photo];
NSLog(#"Finished adding photo to shared manager for URL %# and photo is %#",url,photo) ;
}) ;
}
NSLog(#"Executing wait statement") ;
dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5
dispatch_async(dispatch_get_main_queue(), ^{
if (completionBlock) {
NSLog(#"Executing completion block after download group complete") ;
completionBlock(error);
}
}) ;
}
The "dispatch_group_enter() does not seem to register" because it hasn't actually been called yet by the time that dispatch_group_wait() is called. Or, rather, it's not guaranteed to have been called. There's a race condition.
This isn't specifically about different queues. It's about concurrency and asynchronicity.
dispatch_async() just means "add a task to a list" with an implicit understanding that something, somewhere, somewhen will take tasks off of that list and execute them. It returns to its caller immediately after the task has been put on the list. It does not wait for the task to start running, let alone complete running.
So, your for loop runs very quickly and by the time it exits, it may be that none of the tasks that it has queued have started. Or, if any have started, it may be that they haven't finished entering the group.
Your code may complete its call to dispatch_group_wait() before anything has entered the group.
Usually, you want to be sure that all relevant calls to dispatch_group_enter() have completed before the call to dispatch_group_wait() is made. The easiest way to do that is to have them all happen synchronously in one execution context. That is, don't put calls to dispatch_group_enter() inside blocks that are dispatched asynchronously.
In my app I open the camera by a picker and after the photo has been taken I'd like to safe it by the following method the assets library. The method freezes after the call of the writeImageToSavedPhotosAlbum.
Without the semaphores the methods work perfectly. But than I miss to receive the assetURL.
+ (NSURL*)safeImageToAssetsLibrary:(UIImage *)image metadata:(NSDictionary *)metadata
{
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
__block NSURL *retAssestURL = nil;
dispatch_semaphore_t semaWaitingForSafeImage = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// safe the image to the assests library
NSLog(#"Safe image to asssets library...");
dispatch_async(queue, ^{
[library writeImageToSavedPhotosAlbum:image.CGImage metadata:metadata completionBlock:^(NSURL *assetURL, NSError *error) {
if (error) {
NSLog(#"Image could not be safed to the assets library: %#", error);
retAssestURL = nil;
}
else {
NSLog( #"Image safed successfully to assetURL: %#", assetURL);
retAssestURL = assetURL;
}
dispatch_semaphore_signal(semaWaitingForSafeImage);
}];
});
dispatch_semaphore_wait(semaWaitingForSafeImage, DISPATCH_TIME_FOREVER);
return retAssestURL;
}
And this is method where I call the safeImageToAssetsLibrary method:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
[picker dismissViewControllerAnimated:YES completion:NULL];
// get chosen image and add thumbnail to collection view
NSURL *imageUrl = info[UIImagePickerControllerReferenceURL];
UIImage *chosenImage = info[UIImagePickerControllerOriginalImage];
// safe image to photo library if the camera has been used
if (picker.sourceType == UIImagePickerControllerSourceTypeCamera) {
imageUrl = [BaseImageHandler safeImageToAssetsLibrary:chosenImage metadata:info[UIImagePickerControllerMediaMetadata]];
}
// UPDATE View and Core Data here...
}
Do not wait. Never, never do what you are doing. You are failing to understand what "asynchronous" is about. It means that you are called back when it's all over in the completion block. So that is where you perform the next step.
Do NOT try to return a value from a method that obtains that value in an asynchronous completion block.
So, here, in writeImageToSavedPhotosAlbum:'s completion block, that is where you receive retAssestURL. So if there is a further step, now do it, there, in the completion block. This could involve calling another method or whatever you like, but the point is, things will now happen in the correct order.
And above all, Do NOT use semaphores (or other trickery) to try to turn asynchronous into synchronous. Asynchronous things are asynchronous for a reason. Use the framework, don't fight it. (Actually, what you are doing with semaphores here is not just fighting the framework but spitting in its eye.)
I am developing an app that contacts a RESTful server to get some data and then with the returned JSON response to display that data.
Using UniRest calls and all is working well. The main call is 'runUnirestRequest'
The uni rest call is an async GCD dispatch call. My problem is that because I am testing locally the call is so quick I can't see the activity indicator rolling. It simply disappears before I can see it.
The GCD block occur within the viewController viewDidLoad call.
What I need to achieve: Have the async unirest call take several seconds to simulate a server response that is slow (Dont want to actually stop the iOS app in its tracks).
Please excuse any coding errors/bad habits, only been doing objective c for a week but am happy for any additional constructive crit. :)
I have tried
sleep(5); // But bad idea as far as I can see.
Also tried
[NSThread sleepForTimeInterval:5.0]; // but this doesn't seem to do anything.
viewDidLoad
- (void)viewDidLoad
{
[super viewDidLoad];
[self createActivityIndicator];
NSLog(#"viewDidLoad");
NSLog(#"viewDidLoad->thread: %#", [NSThread currentThread]);
[messageLabel setText:#""];
unirestQueue = dispatch_queue_create("com.simpleweb.pbs.dayDataUnirestRequest", NULL);
// Do any additional setup after loading the view from its nib.
daySalesFigures = [[PBSDaySales alloc] init];
responseVal = [[HttpJsonResponse alloc] init];
// Use Grand Central Dispatch to run async task to server
dispatch_async(unirestQueue, ^{
[self runUnirestRequest:self.requestUrl];
});
dispatch_after(unirestQueue, dispatch_get_main_queue(), ^(void){
[activityIndicator stopAnimating];
});
}
runUniRestRequest function
- (void) runUnirestRequest:(NSString*)urlToGet
{
[NSThread sleepForTimeInterval:5.0];
NSLog(#"runUnirestRequest called");
HttpJsonResponse* response = [[Unirest get:^(SimpleRequest* request) {
[request setUrl:#"http://x.x.x.x:9000/Sales/Day/2013-02-14"];
}] asString];
NSString *jsonStr = [response body];
SBJsonParser *jsonParser = [SBJsonParser new];
id response2 = [jsonParser objectWithString:jsonStr];
[self deserializeJsonPacket:(NSDictionary*)response2];
}
dispatch_after's first parameter is time. You are passing in unirestQueue, which is dispatch_queue_t queue according to
unirestQueue = dispatch_queue_create("com.simpleweb.pbs.dayDataUnirestRequest", NULL);
proper code for dispatch_after, i.e. performing block after some delay, is like this:
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// Do whatever you want
});
Edit: Oh, I probably see what you are trying to accomplish :-) You thought the dispatch_after means "do something after this queue" right? Nope, it's "do something after some time"
Edit 2: You can use code like below to do something time consuming in background and update UI when its done
// Start block on background queue so the main thread is not frozen
// which prevents apps UI freeze
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Do something taking a long time in background
// Here we just freeze current (background) thread for 5s
[NSThread sleepForTimeInterval:5.0];
// Everything in background thread is done
// Call another block on main thread to do UI stuff
dispatch_async(dispatch_get_main_queue(), ^{
// Here you are in the main thread again
// You can do whatever you want
// This example just stops UIActivityIndicatorView
[activityIndicator stopAnimating];
});
});
Edit 3: I recommend this great article about GCD at raywenderlich.com for more detailed info
So my Twitter/Facebook implementation in my app has been a learning experience, but I'm almost there and I have one last, probably simple question. Using the MGTwitter engine, I'm calling a method from my viewcontroller in
- (void) setAccessToken: (OAServiceTicket *) ticket withData: (NSData *) data {
The method is firing off, (confirmed by NSLog calls). However, it's not doing what it's supposed to do, which is fade in my logout button for Twitter. I'm still getting my hands around the way Objective-C handles methods and all, I feel like I'm just not pointing my variables to the right place. Any direction is much appreciated, here is the code below:
SA_OAuthTwitterEngine.m -
//
// access token callback
// when twitter sends us an access token this callback will fire
// we store it in our ivar as well as writing it to the keychain
//
- (void) setAccessToken: (OAServiceTicket *) ticket withData: (NSData *) data {
if (!ticket.didSucceed || !data) return;
NSString *dataString = [[[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding] autorelease];
if (!dataString) return;
if (self.pin.length && [dataString rangeOfString: #"oauth_verifier"].location == NSNotFound) dataString = [dataString stringByAppendingFormat: #"&oauth_verifier=%#", self.pin];
NSString *username = [self extractUsernameFromHTTPBody:dataString];
if (username.length > 0) {
[self setUsername: username password: nil];
if ([_delegate respondsToSelector: #selector(storeCachedTwitterOAuthData:forUsername:)]) [(id) _delegate storeCachedTwitterOAuthData: dataString forUsername: username];
}
[_accessToken release];
_accessToken = [[OAToken alloc] initWithHTTPResponseBody:dataString];
//Call twit login from my view controller
MyView *fvController = [[MyView alloc] init];
[MyView twitLogin];
[MyView helper];
NSLog(#"LETS TWEET DIRECTLY AFTER SUCCESSFUL LOG IN!");
}
This is what my helper method is doing in my .m file:
-(void)helper{
NSLog(#"HELPER FUNCTION");
[self fadeIn:twitterLogout withDuration:2 andWait:2.0];
}
This is the method it's calling
//FADE IN FUNCTION ------------------------------//////////////////////
-(void)fadeIn:(UIView*)viewToFadeIn withDuration:(NSTimeInterval)duration
andWait:(NSTimeInterval)wait
{
[UIView beginAnimations: #"Fade In" context:nil];
[UIView setAnimationDelay:wait];
[UIView setAnimationDuration:duration];
viewToFadeIn.alpha = 1;
[UIView commitAnimations];
}
In Objective-C, methods are declared in one of two ways:
- (returnType)methodName;
or
+ (returnType) methodName;
The first type is an "instance" method and the second type is a "class" method.
These lines should be changed:
[MyView twitLogin];
[MyView helper];
Try this instead:
[fvController twitLogin];
[fvController helper];
Additionally, you may be calling your helper method before the delegate returns a value. You should see if the MGTwitterEngine contains a delegate. (I'd be surprised if it didn't.) You should use the available callbacks to call methods only when the login is finished. Simply calling the methods in order won't do what you want.
*Are both the log statements printed?
*Is setAccessToken:withData: being called from the main thread? Try calling the fadeIn method from the main thread i.e. something like this
- (void) helper{
NSLog(#"HELPER FUNCTION");
[self performSelectorOnMainThread:#selector(callFadeIn)];
}
-(void)callFadeIn{
[self fadeIn:twitterLogout withDuration:2 andWait:2.0];
}
See if that helps. It is possible that setAccessToken: (and thus helper and fadeIn) is being called from another thread. All UI operations should happen from the main thread.
I want to put in a timeout in case it takes too long to find my location, send out the relevant url, and parse the xml. It worked when I used performSelector:withObject:afterDelay in the locationManager (just to test getting the xml), but when I put similar code around my parser it doesn't actually abort the parsing. I am testing this by dropping the delay to 0.01.
My problem is: even with the delay set to 0.01, it still waits for all the parsing to complete first, and only then does it put up the alertView that is coded in the parsingDidTimeout method.
I did try this with a timer, and that wasn't working as well as performSelector: does in the other parts of my code. Either way, it doesn't put up the alertView, and stop the parsing, until after the parsing has finished, no matter how long that takes.
I create a url which requires a radius. First I try a small radius, but if I don't get the data I need, I expand the radius and send the url again and parse again. Here is part of my StartParsing method.
xmlParser = [[NSXMLParser alloc] initWithContentsOfURL:url];
XMLParser *parser = [[XMLParser alloc] initXMLParser];
[xmlParser setDelegate:parser];
if (!hadToExpandRadius){//meaning, only do this the first time I send out the url and parse
[self performSelector:#selector(parsingDidTimeout:) withObject:nil afterDelay:0.01];
}
//Start parsing the XML file.
BOOL success = [xmlParser parse];
if(success){
if((didNotGetTheDataYet) && (radius < 500)){
hadToExpandRadius = YES;
radius = radius + 35;
[self startParsing];//do this same method, with larger radius
}
else {
NSLog(#"No Errors");
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(parsingDidTimeout:) object:nil];}
[parser release];
}
-(void)parsingDidTimeout{
[xmlParser abortParsing];
UIAlertView *servicesDisabledAlert = [[UIAlertView alloc] initWithTitle:#"Try Later" message:#"We need a better connection. We can get the data later." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[servicesDisabledAlert show];
[servicesDisabledAlert release];
[myActivityView stopAnimating];
}
Thank you for your help.
Calling performSelector:withObject:afterDelay: you ask the run loop to call the selector later. But [xmlParser parse] blocks the run loop, so it doesn't have a chance to call you selector.
abortParsing is designed to be called inside parsers' delegate methods.
The workaround can be to parse in a separate thread.
Found it -- just extra ":" in my performSelector:#selector(parsingDidTimeout:)!
I thought it was something fancy having to do with the second thread. Just syntax.
Thanks for explaining about the parse blocking the run loop. I was hoping not to need another thread, but your suggestion fixed my problem. Thanks.