in my iOS app I do the following.
viewDidAppear(){
// Load a spinner in a view on the top
[DSBezelActivityView newActivityViewForView:self.view];
// Execute code that require 3 seconds
...
// Stop the spinner
[DSBezelActivityView removeViewAnimated:YES];
}
The problem is that the spinner doesn't appear, because the the cpu is working hard (something similar). It's like that the code betweek the start and stop has precedence on the rendering of the view.
I would love to find a way to show effectively the start of the spinner, without using a timer to delay the code execution.
Thanks
If you have a method like
-(void) showSpinner:(UIView*)view {
dispatch_async(dispatch_get_main_queue(), ^{
[DSBezelActivityView newActivityViewForView:view];
});
}
there are several ways to call it from a different thread. Choose one from the following:
[NSThread detachNewThreadSelector:#selector(showSpinner:) toTarget:self withObject:self.view];
// or
[self performSelectorInBackground:#selector(showSpinner:) withObject:self.view];
// or
NSInvocationOperation *invOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(showSpinner:) object:self.view];
NSOperationQueue *opQueue = [[NSOperationQueue alloc] init];
[opQueue addOperation:invOperation];
// or
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self showSpinner:self.view];
});
Alt + click for details.
Move code between start and stop activity indicator into separate thread because it's blocking main thread. That's why activity indicator is not showing.
Edit: Example
I agree with the 1st answer with a couple of modifications. I just went through this exact same problem. The problem is that anything graphical is going to automatically move to the background updating when you have code that takes time to get through. Throwing the spinner to the background is what it essantially doing anyway. What you want is (sadly) for you main code to run in the background and the spinner to run in the foreground. I know this sounds bad, but in some cases allowing your code to run a bit slower to give indication that the app is doing something useful is beneficial to the user.
In order to get the spinner to work:
1) Take all the code that takes the 3 seconds to run, and put that into a function that is a void function
2) Instantiate your spinner but store it to a variable that is accessible outside your viewDidAppear routine.
3) Startup a new NSTimer with that runs continuously with an increment of about every quarter second or so. I will define what goes into the routine that gets called every cycle later.
4) Call the routine you created in step 1 using the performSelectorInBackground capability. This essentially is now going to run your startup (3 seconds worth) in the background which is really the only way to allow the animated spinner to show up and truly animate.
5) In the routine you created in step 1, add a line of code right at the top that updates a (global to the object) boolean to true, stating that we are in the middle of our main 3 second routine.
6) At the end of the routine defined in step 1 add a line of code setting the same global defined in step 5 to false indicating that our 3 second routine is completed.
7) In the timer routine, we now want to do something that looks like the following:
// If busy that start the spinner
if(YES == busy){
[spinner startAnimating];
}else{
[spinner stopAnimating];
// Here we can also stop and deallocate the timer
}
If you need more aid on this subject, I can indeed provide exact code. Take a look at the example app that I have developed for the Pepperdine News Group. When you press a button, the spinner comes up on the top right of the screen.
http://itunes.apple.com/us/app/pepperdine-graphic-for-iphone/id516343215?mt=8
Related
I have a UIView *loadingView that simply has a black BG and a UIActivityIndicator. I want to add this to my view, and then sleep the thread for a couple seconds:
[self.window addSubview:loadingView];
loadingView.hidden=NO;
sleep(2);
Apparently, this isn't enough time to add the view-the thread sleeps but the view isn't added, and only appears after the 2 seconds. So I tried something like this:
-(void)sleep
{
sleep(2);
}
[self.window addSubview:loadingView];
loadingView.hidden=NO;
[self performSelector:#selector(sleep) withObject:nil afterDelay:0.01];
This displays the view before the sleep, which is the desired result. However, under these 3 lines there are several lines of code that I don't want executed until after the sleep, and within the 0.01 seconds, all those lines are executed.
Why on earth are you sleeping the main thread for 2 seconds? Normally that's a horrible idea, because your app will become completely unresponsive for 2 seconds.
You have the right idea in using performSelector:withObject:afterDelay:. But there is no way to get around the fact: if you want something to happen after that delay, then it has to go in the method called after the delay. Just move those other lines of code after the sleep().
(And really, if you want something to happen after two seconds, just use performSelector:withObject:afterDelay: with a delay of two seconds.)
I ended up just doing this:
-(void)addLoadingView
{
[self.window addSubview:loadingView];
loadingView.hidden=NO;
}
//some function...
[self performSelectorInBackground:#selector(addLoadingView) withObject:nil];
sleep(2);
I'm new to threading, so is what I'm doing here safe?
I have to stop and restart a simple method that takes from 1 to 2 seconds to execute. How can this be accomplished? I have already tried [NSObject cancelPreviousPerformRequestsWithTarget:self] but it works only with performselector after delay. I have also tried to create a new thread but it doesn't seem to work...
This is my method:
-(IBAction)MyMethod
{
NSLog(#"start");
//Here is the code that takes time to execute. It regards UI intervention,graphic calculation, x and y position etc.
NSLog(#"end");
}
I want this effect: one click on the linked UIButton and the method start (so print start log and end log). If I click the linked UIButton before the NSLog is printed, the method must stop. Is this possible?
You'll want to use a background task. I would suggest subclassing and using a NSOperation and checking for isCancelled within the body of main. See Apple's documentation on using NSOperation and NSOperationQueue.
Well, to do this with threads what I usually do is dividing things into 3 sections:
start.
processing.
finish.
And it looks like this:
-(void)start:(id)sender{
//prepare everything and anything
[NSThread detachNewThreadSelector:#selector(processing:) toTarget:self withObject:nil];
}
-(void)processing:(id)sender{
//Perform all your calculations, you can't modify UI elements here
[self performSelectorOnMainThread:#selector(finish:) withObject:nil waitUntilDone:NO];
}
-(void)finish:(id)sender{
//Wrap everything up and do any modifications to the UI
}
Now, to cancel this you could add maybe use:
Cancels perform requests previously registered with
performSelector:withObject:afterDelay:.
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(id)anArgument
How would I make an image appear after 3 seconds?
You can use:
[self performSelector: withObject: afterDelay: ]
I'm a big fan of using GCD (iOS 4+) because you can simplify your code with inline blocks.
In your case, you should set the image to hidden in Interface Builder, then create an IBOutlet with a connection to an ivar in your class.
Then you can simply run this in viewDidLoad or similar:
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 3.0);
dispatch_after(delay, dispatch_get_main_queue(), ^(void){
yourImage.hidden = NO;
});
This assumes that you are calling performSelector:withObject:afterDelay from the main thread, and that your UIImageView is initially hidden.
//assumes theImageView.hidden = YES
[self performSelector:#selector(showImage:) withObject:theImageView afterDelay:yourTimeInterval];
-(void)showImage:(UIImageView*)anImageView {
anImageView.hidden = NO;
}
It is important that performSelector is called from the main thread because the selector that is called after the delay will run on the same thread, and you do not want to update UI from anything other than the main thread as a general rule.
I haven't used XCode in awhile, but I'll take a stab for ya..
In your Interface Builder set the image's visibility as hidden
When your app starts up, set some global variable to the current time in an init fxn
In the main control loop for your UI, check if that global var contains a time that is more than 3 seconds ago, if so, change that image's visibility parameter to shown.
Best I can really say without really taking a look, which isn't possible right now.
Good luck!
I have the following code:
[self.view bringSubviewToFront:loggingIn];
[self loginWithUsername:user Password:pw];
This is inside an IBAction method for a button press. The button stays highlighted until the user is logged in (this could take several seconds) and THEN the view appears. How come the view isn't appearing before the login method is called?
John's answer would work, but it has to do with the Run Loop than blocking threads. Basically, whenever your app receives an event such as a button getting clicked, your handler gets run in the main thread. Any changes you make to the UI happen after your handler finishes. This lets Cocoa optimize the graphics updates by analyzing/performing them together.
Run Loop:
Event -> Your Handler Code -> System Updates UI
So you need to let your handler code finish in order to let the system start updating the UI - before invoking your loginWithUsername:Password: [sic] method. Scheduling a timer will bump it to the next iteration of the run loop (the timer going off becomes the "event" that triggers the next loop). So,
Run Loop (2 iterations):
Mouse Click -> Your Handler Code (reorder views) -> System reorders views
Timer Fires -> Your Handler Code (trigger login) -> Any addl UI updates
So do something like this:
- (void) buttonHandler: (id)sender {
[self.view bringSubviewToFront:loggingIn];
[self performSelector: #selector(performLogin)
withObject: [NSArray arrayWithObjects:user, pw, nil]
afterDelay: 0];
}
- (void) performLogin: (NSArray *)args {
[self loginWithUsername:[args objectAtIndex:0] Password:[args objectAtIndex:1]];
}
It doesn't have to be an array. You could also pack your username and password in a dictionary and use that as your argument instead.
The first call runs in a different thread which is blocked by the second line. Try delaying the second call by .1 seconds or so by using the:
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
method of NSObject. Since you are using two arguments you will need to either pass both as a single object or use instance variables.
I am working on a web-services data processing app and I am trying to make the app run as quickly as possible. When a certain 3 finger pan gesture is performed, I call a method that sends updated information off to the server to get a new batch of images to update the existing ones with.
So lets say there are 15 images in an array, I filter through them with a 2 finger gesture, and then if I want to change something about them, I can do the 3 finger gesture, and I get that same set back, just tweaked a bit (contrast/brightness, etc.).
Is what I want though is to be able to update the imageView that is displaying the images after the first image has been retrieved, so as to give the user a feel for what the rest in the series are going to look like. But no matter what I try, and no matter how many different threads I try and implement, I can't get the imageView to update before the entire download is complete. Once the batch download is done (which is handled on a separate thread) the imageView updates with the new images and everything is great.
The first step in the process is this:
if(UIGestureRecognizerStateEnded == [recognize state]){
[self preDownload:windowCounter Level:levelCounter ForPane:tagNumber];// Where this method is what gets the first image, and tries to set it to the imageView
[self downloadAllImagesWithWL:windowCounter Level:levelCounter ForPane:tagNumber]; //And this method goes and gets all the rest of the images
}
This is my preDownload method:
-(void)preDownload:(int)window Level:(int)level ForPane:(int) pane{
int guidIndex = [[globalGuids objectAtIndex:pane] intValue];
UIImage *img = [DATA_CONNECTION getImageWithSeriesGUID:[guids objectAtIndex:guidIndex] ImageID:counter Window:window Level:level];
if(pane==0){
NSLog(#"0");
[imageView3 setImage:img];
}else if(pane==1){
NSLog(#"1");
[imageView31 setImage:img];
}else if(pane==2){
NSLog(#"2");
[imageView32 setImage:img];
}else if(pane==3){
NSLog(#"3");
[imageView33 setImage:img];
}
}
So by separating this out into two different methods (there are no threads being implemented at this point, these methods are being called before all that) I was thinking that after the preDownload method completed, that the imageView would update, and then control would continue on down into the downloadAllImagesWithWL method, but that doesn't appear to be the case.
Am I missing something simple here? What can I do to update my GUI elements before that second method is through running?
You are right. However the viewn won't refresh until your code reaches runloop. You can do 2 things:
Make your downloadAllImagesWithWL method async, so it will return after you called it, your main thread reaches runloop, gui updates, and the download method will tell your logic through a callback when its done.
OR
A simplier hackier (and bad) solution would be to run runloop for some time before you call your download method. Something like this: [[NSRunloop currentRunLoop] runUnitlDate: [Date dateWithTimeIntervalSinceNow: 0.1]]; It will run runloop for 0.1 second.
When the image is set, the image view will mark itself as needing display. The actual display won't occur until the beginning of the next run loop. In OS X, you can use -display to draw the view immediately, but I don't think Apple created a public method to do this on iOS. However, if the next method simply creates the background thread, then it will return quickly and the display update will probably occur before the thread finishes.