In Grand Central Dispatch I want to start a spinner - UIActivityIndicatorView - spinning prior to beginning long running task:
dispatch_async(cloudQueue, ^{
dispatch_async(dispatch_get_main_queue(),
^{
[self spinnerSpin:YES];
});
[self performLongRunningTask];
dispatch_async(dispatch_get_main_queue(),
^{
[self spinnerSpin:NO];
});
});
Here is the spinnerSpin method:
- (void)spinnerSpin:(BOOL)spin {
ALog(#"spinner %#", (YES == spin) ? #"spin" : #"stop");
if (spin == [self.spinner isAnimating]) return;
if (YES == spin) {
self.hidden = NO;
[self.spinner startAnimating];
} else {
[self.spinner stopAnimating];
self.hidden = YES;
}
}
One thing I have never seen discussed is the difference - if any - between [myView setNeedsDisplay] and [myActivityIndicatorView startAnimating]. Do they behave the same?
Thanks,
Doug
The [UIView setNeedsDisplay] method has nothing to do with a UIActivityIndicatorView's animation state.
setNeedsDisplay simply informs the system that this view's state has changed in a way that invalidates its currently drawn representation. In other words, it asks the system to invoke that view's drawRect method on the next drawing cycle.
You very rarely need to invoke setNeedsDisplay from outside of a view, from code that is consuming the view. This method is meant to be invoked by the view's internal logic code, whenever something changes in its internal state that requires a redraw of the view.
The [UIActivityIndicatorView startAnimating] method is specific to the UIActivityIndicatorView class and simply asks the indicator to start animating (e.g. spinning). This method is instant, without requiring you to call any other method.
On a side note, you could simplify your code by simply calling startAnimating or stopAnimating without manually showing/hiding it. The UIActivityIndicatorView class has a hidesWhenStopped boolean property that defaults to YES, which means that the spinner will show itself as soon as it starts animating, and hide itself when it stops animating.
So your spinnerSpin: method could be refactored like this (as long as you haven't set the hidesWhenStopped property to NO):
- (void)spinnerSpin:(BOOL)spin {
if (YES == spin) {
[self.spinner startAnimating];
} else {
[self.spinner stopAnimating];
}
}
Related
I have a function called "openButtonPressed" which gets executed when a button on the ui gets pressed. Now I would like to show a loading view at first and then execute the segue. For some reason, the segue always gets called first.
Does somebody have a clue?
Thank you!
- (void)openButtonPressed:(id)sender
{
NSDebug(#"Open Button");
[self showLoadingView];
static NSString* segueToContinue = kSegueToContinue;
if ([self shouldPerformSegueWithIdentifier:segueToContinue sender:self]) {
[self performSegueWithIdentifier:segueToContinue sender:self];
}
}
- (void)showLoadingView
{
if(!self.loadingView) {
self.loadingView = [[LoadingView alloc] init];
[self.view addSubview:self.loadingView];
}
[self.loadingView show];
}
It looks like LoadingView is being initialized without a size.
It also may be executing the segue very quickly and you don't have any time to see the loading view.
You can set a breakpoint at the performSegue call and use the view debugger in Xcode to confirm if the view was loaded and what size the view was initialized to.
Maybe the view is not in the right place or in the right size. But even if you fix that you need to add a delay so the user can see the loading... I replicated your scenario here and after giving the view a initWithFrame it appears, but cannot be seen because is too fast...
If you want to show the loading for a period of time before the performSegue, you could use the performSelector:withObject:afterDelay.
If you need to wait an action from the loadingView, create a completionHandler in the loadingView initializer...
The hints below were very helpful to me and totally a better solution but I ended up using [CATransaction flush]; to force the refresh.
- (void)openButtonPressed:(id)sender
{
NSDebug(#"Open Button");
[self showLoadingView];
[CATransaction flush];
static NSString* segueToContinue = kSegueToContinue;
if ([self shouldPerformSegueWithIdentifier:segueToContinue sender:self]) {
[self performSegueWithIdentifier:segueToContinue sender:self];
}
}
I'm delaying UIKit messages as per this SO answer
Now another requirement has arisen, instead of just queuing SSHUDView method calls we should also handle queing of UIAlertView. For example one scenario could be that we display a hud then after 1 second we display another hud and then finally after 1 second we display a UIAlertView.
The problem is now that since the SSHUDViews are run asynchronously on a background thread when I get to display the UIAlertView the SSHUDViews have not finished displaying so the UIAlertView will overlay the hud.
Basically I need a way to queue up and delay methods whether they are of class SSHUDView or UIAlertView. A feedback queue, where you can delay individual messages.
What you're talking about sounds like a perfect fit for semaphores (see under the heading Using Dispatch Semaphores to Regulate the Use of Finite Resources)! I saw the SO Answer you linked to and I don't think it solves the case of UIView animations. Here's how I'd use semaphores.
In your view controller add an instance variable dispatch_semaphore_t _animationSemaphore; and initialize it in the - init method:
- (id)init
{
if ((self = [super init])) {
_animationSemaphore = dispatch_semaphore_create(1);
}
return self;
}
(Don't forget to release the semaphore in the - dealloc method using dispatch_release. You also might want to wait for queued animations to finish by using dispatch_semaphore_wait, but I'll leave that for you to figure out.)
When you want to queue up an animation, you'll do something like this:
- (void)animateSomething
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
dispatch_semaphore_wait(_animationSemaphore, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.5 animations:^{
// Your fancy animation code
} completion:^(BOOL finished) {
dispatch_semaphore_signal(_animationSemaphore);
}];
});
});
}
You can use the - animateSomething template to accomplish different things, such as displaying an SSHUDView or a UIAlertView.
What you're describing sounds like an animation. Why not just use UIView animation and chain the series of animation blocks:
[UIView animateWithDuration:2
animations:^{
// display first HUD
}
completion:^(BOOL finished){
[UIView animateWithDuration:2
animations:^{
// hide first HUD, display second HUD
}
completion:^(BOOL finished){
[UIView animateWithDuration:2
animations:^{
// hide second HUD, show UIAlert
}
completion:nil
];
}
];
}
];
I have several dispatch_async methods linked so they authenticate a user in my system.
The next async method only happens when the previous one finishes, as they each have completion handlers.
When the last one is finished I perform a custom segue with 2 uiview animation blocks.
However, when I log when each of these actually run, there is a considerable gap between the log and the animation actually happening, eventually the views animate and the completion block is called.
I don't really know how useful it would be adding my code here, but i have tested and it must be the async methods because if I comment them out and just return YES the animations happen without delay at the same time as the log.
Does anyone know why this might happen?
EDIT *(with code)
Typical 'exist check' used for email, user, user id.
- (void)existsInSystemWithCompletionHandler:(void (^)(BOOL))block
{
self.existsInSystem = NO;
if (self.isValid) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//
// Get data
//
if (dataIsValid) {
block(YES);
} else {
block(NO);
}
});
} else {
block(self.existsInSystem);
}
}
Check user exists
[potentialUser existsInSystemWithCompletionHandler:^(BOOL success) {
if (success) {
// Perform segue
//
[self performSegueWithIdentifier:#"Logging In" sender:self];
}
}];
Segue
- (void)perform
{
NSLog(#"Perform");
LogInViewController *sourceViewController = (LogInViewController *)self.sourceViewController;
LoggingInViewController *destinationViewController = (LoggingInViewController *)self.destinationViewController;
destinationViewController.user = sourceViewController.potentialUser;
// Animate
//
[UIView animateWithDuration:0.2f
animations:^{
NSLog(#"Animation 1");
//
// Animate blah blah blah
//
}];
[UIView animateWithDuration:0.4f
delay:0.0f
options:UIViewAnimationOptionCurveEaseIn
animations:^{
NSLog(#"Animation 2");
//
// Animate blah blah blah
//
}
completion:^(BOOL finished) {
NSLog(#"Completion");
//
// Finished
//
[sourceViewController presentViewController:destinationViewController animated:NO completion:nil];
}];
}
LoggingIn VC
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.user loginWithCompletionHandler:^(BOOL success) {
if (success) {
[self performSegueWithIdentifier:#"Logged In" sender:self];
}
}];
}
Fixed! It seems to work now that in my code I have added the lines:
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock(success);
});
From your description it seems likely that your segue is waiting for a process to finish.
Maybe your nested async methods that follow each other through their completion methods have produced somewhat convoluted code. This might be the reason why you could be overlooking the blocking method.
One way to tidy up your code is use a sequential queue. The blocks pushed to the queue will start only when the previous one has finished.
when i click on button which title is click here to enlarge then i want show activity indicator on the first view and remove when load this view.
but i go back then it show activity indicator which is shown in this view.
in first vie .m file i have use this code for action.
-(IBAction)btnSelected:(id)sender{
UIButton *button = (UIButton *)sender;
int whichButton = button.tag;
NSLog(#"Current TAG: %i", whichButton);
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[spinner setCenter:CGPointMake(160,124)];
[self.view addSubview:spinner];
[spinner startAnimating];
if(whichButton==1)
{
[spinner stopAnimating];
first=[[FirstImage alloc]init];
[self.navigationController pushViewController:first animated:YES];
[spinner hidesWhenStopped ];
}}
in above code i have button action in which i call next view. Now i want show/display activity indicator when view upload. In next view i have a image view in which a image i upload i have declare an activity indicator which also not working. How do that?
Toro's suggestion offers a great explanation and solution, but I just wanted to offer up another way of achieving this, as this is how I do it.
As Toro said,
- (void) someFunction
{
[activityIndicator startAnimation];
// do computations ....
[activityIndicator stopAnimation];
}
The above code will not work because you do not give the UI time to update when you include the activityIndicator in your currently running function. So what I and many others do is break it up into a separate thread like so:
- (void) yourMainFunction {
activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[NSThread detachNewThreadSelector:#selector(threadStartAnimating) toTarget:self withObject:nil];
//Your computations
[activityIndicator stopAnimating];
}
- (void) threadStartAnimating {
[activityIndicator startAnimating];
}
Good luck!
-Karoly
[self.navigationController pushViewController:first animated:YES];
Generally, when you push a view controller into navigation controller, it will invoke the -(void)viewWillAppear: and -(void)viewDidAppear: methods. You can add activity indicator view inside the viewWillAppear: and call startAnimation of indicator view. You CANNOT invoke startAnimation and stopAnimation at the same time. For example,
- (void)viewWillAppear:(BOOL)animated
{
[aIndicatorView startAnimation];
// do somethings ....
[aIndicatorView stopAnimation];
}
Because the startAnimation and stopAnimation are under the same time, then no animation will show.
But if you invoke startAnimation in -(void)viewWillAppear: and invoke stopAnimation in another message, like followings.
- (void)viewWillAppear:(BOOL)animated
{
[aIndicatorView startAnimation];
// do somethings...
}
- (void)viewDidAppear:(BOOL)animated
{
[aIndicatorView stopAnimation];
}
Because viewWillAppear: and viewDidAppear: are invoked with different event time, the activity indicator view will work well.
Or, you can do something like followings:
- (void)viewWillAppear:(BOOL)animated
{
[aIndicatorView startAnimation];
// Let run loop has chances to animations, others events in run loop queue, and ... etc.
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
// do somethings ....
[aIndicatorView stopAnimation];
}
The above example is a bad example, because it invokes two or more animations in the -runUntilDate:. But it will let the activity indicator view work.
Create a webview. Add a activity indicator to the webview. If you are loading a image via url into the webview then implement the webview delegate methods. Once the url is loaded then stopanimating the activity indicator.
Let me know which step you are not able to implement.
This is very strange. My view is receiving a notification with an index number that I use to find a UIbutton by tag and then change its text label. The only trouble is the text will not change. I've put in break points and the code is being executed.
Is there some peculiarity with variable scopes when using notifications?
- (void)receiveEvent:(NSNotification *)notification {
int pass = [[[notification userInfo] valueForKey:#"index"] intValue];
NSLog(#"button index is %i", pass);
UIButton *changebut = (UIButton *) [self.view viewWithTag:pass];
ButtonStatusModel *m= [self.buttoneventpendingarray objectAtIndex:pass];
NSLog(#"current but status = %i",m.btnstatus);
if (m.btnstatus==3 ) {
//this should change the label but it does not;
[changebut setTitle:#"playing" forState:UIControlStateNormal];
m.btnstatus=2;
NSLog(#"should set text to 'playing");
//[engine addActionToQue:buttonid actionSample:actionsample action:1];
}else if (m.btnstatus==1) {
//this should change the label but it does not;
[changebut setTitle:#"idle" forState:UIControlStateNormal];
NSLog(#"should set text to 'idle");
m.btnstatus=0;
//[engine addActionToQue:buttonid actionSample:actionsample action:0];
}
}
EDIT 1
Thanks to commenters I've determined that even though my reciveevent function in on my main view controller the function is not being executed from the main thread (dont really understand this) and so thats why the button label will not change.
I used the following code to determine if it was the main thread or not. I think now to change the button label I need to call perform selector on main thread? Is this the correct thing to do? If so I need to pass a function 2 variables, I dont see how perform selector on main thread accomodates this.
-(void) updatebutton:(int)tag changeto:(int) val
{
if ([NSThread isMainThread]) {
{
NSLog(#"is main thread");
}
} else {
NSLog(#"is not thread");
[self performSelectorOnMainThread:#selector( /*function with 2 parameters here*/) withObject:nil waitUntilDone:YES];
}
}
EDIT 3*
Using blocks instead of main selector helped me out of my problem.
I really don't understand the whole main thread though. I assumed that if code was in my view controller that any executed code would be performed on the main thread. That seems to be big fat wrong. I would appreciate a little enlightenment on the matter.
Heres what I used anyway
-(void) updatebutton:(int)tag changeto:(int) val
{
UIButton *changebut = (UIButton *) [self.view viewWithTag:tag];
if ([NSThread isMainThread]) {
{
NSLog(#"is main thread");
if (val==2) {
[changebut setTitle:#"playing" forState:UIControlStateNormal];
}
if (val==0) {
[changebut setTitle:#"idle" forState:UIControlStateNormal];
}
}
} else {
NSLog(#"is not thread");
//[self performSelectorOnMainThread:#selector(updatebutton::) withObject:nil waitUntilDone:YES];
if (val==2) {
dispatch_async(dispatch_get_main_queue(), ^{
[changebut setTitle:#"playing" forState:UIControlStateNormal];
});
}
if (val==0) {
dispatch_async(dispatch_get_main_queue(), ^{
[changebut setTitle:#"idle" forState:UIControlStateNormal];
});
}
}
}
Try putting
[changebut setTitle:#"idle" forState:UIControlStateNormal];
in the -viewDidAppear method. If it doesn't work either, make sure that all the outlets are connected in IB. Moreover, try adding a few spaces as the button title in your .xib file (if you have one).
Is -receiveEvent called on another than the main thread? UI changes must be performed on the main thread.