Wait until UI has updated before removing UIActivityIndicator - objective-c

I am having an issue with an iPad app I am developing. I have the following code:
CFTimeInterval startTime = CFAbsoluteTimeGetCurrent();
[self showSpinnerWithMessage:#"Loading settings..."]; // Shows a UIActivityIndicator on screen
dispatch_async(dispatch_get_global_queue(0, 0), ^
{
//Updating items on screen here
[label1 setText:#"Test1"];
[label2 setText:#"Test3"];
//...
CFTimeInterval difference = CFAbsoluteTimeGetCurrent() - startTime;
if (difference < MINIMUM_INDICATOR_SECONDS)
{
[NSThread sleepForTimeInterval:MINIMUM_INDICATOR_SECONDS - difference];
}
dispatch_async(dispatch_get_main_queue(), ^(void)
{
[self removeSpinner]; // Removes UIActivityIndicator from screen
});
};
The problem is that sometimes the UI takes a while to update, and in these particular instances the spinner (UIActivityIndicator) goes away before the UI has actually updated. I realize this is because the items on screen do not actually update until the next run loop so my question is, how can make my [self removeSpinner] call wait until the UI has updated?

Try putting the UI-updating code in the block with removeSpinner.
dispatch_async(dispatch_get_main_queue(), ^(void)
{
[label1 setText:#"Test1"];
[label2 setText:#"Test3"];
[self removeSpinner]; // Removes UIActivityIndicator from screen
});

Put a [self.view setNeedsDisplay]; just before the [self removeSpinner].

Related

Repeating UIButton animation and change background

Can someone please tell me why this doesn't work?:
//Changing the Images
- (void) squareOneColour {
NSUInteger r = arc4random_uniform(5);
[self.squareOne setBackgroundImage:[self.colorArray objectAtIndex:r] forState:UIControlStateNormal];
}
//Moving buttons
- (void) squareOneMover {
NSUInteger r = arc4random_uniform(3);
[self.squareOne setHidden:NO];
CGPoint originalPosition = self.squareOne.center;
originalPosition.y = -55;
[self.squareOne setCenter:originalPosition];
CGPoint position = self.squareOne.center;
position.y += 760;
[UIView animateWithDuration:r + 3
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
[self.squareOne setCenter:position];
}
completion:^(BOOL complete) {
if (complete) {
[self squareOneColour];
[self squareOneMover];
}
}
];
}
I am trying to get a UIButton to animate down the screen and when the animation is finished, for it to repeat but with a different background. The code above seems it should work but the second animation is never completed. (The UIButton is animated once, then everything stops). I have tried putting the code shown below (into the completion block), with an outside NSUInteger method creating a random number for r but it never works. But it does work when I replace r with a different number, like 1 or 2.
I found the answer. After playing around with it, I found that the function I wanted the UIButton to do was only fit for a UIImageView. I guess I'll have to make an invisible button over the UIImageView
You're stepping on your own feet. Step out to the next cycle of the runloop and all will be well:
[self squareOneColour];
dispatch_async(dispatch_get_main_queue(), ^{
[self squareOneMover];
});

Objective-C: Activity Indicator during method call

I have a method which checks for bluetooth devices ([service scanForSensBoxes]).
The method scanForSensBoxes has a timeout of 5 seconds. During this time I would like to animate an Activity Indicator.
There for I start the spinning in beginn of the attached method, and stop it after the call of "scanForSensBoxes".
Apparently this doesn't work. The result of the code I have at the moment is, that the start and stop animation of the spinner is only processed when the "buttonScanPressed" method is completed instead of during the processing of it. Means the spinner never does animate.
How do I have to change the approach to that, so the spinner will animate during the call of "buttonScanPressed"?
Thanks for any help.
- (IBAction)buttonScanPressed:(id)sender {
[_waitingSpinner startAnimating];
SensBoxServiceLib *service = [[SensBoxServiceLib alloc]init];
NSString *tmp=[NSString stringWithFormat:#"%d",[service scanForSensBoxes]];
[_waitingSpinner stopAnimating];
_sensBoxCount.text=tmp;
}
If scanForSensBoxes is blocking the main thread, the activity indicator won't animate. Assuming this is this case you need to perform the blocking activities within that method on a background queue.
-(IBAction)buttonScanPressed:(id)sender {
// animate activity indicator
[_waitingSpinner startAnimating];
// perform blocking activity in background
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) {
SensBoxServiceLib *service = [[SensBoxServiceLib alloc] init];
NSString *tmp = [NSString stringWithFormat:#"%d", [service scanForSensBoxes]];
// perform UI updates on main thread
dispatch_async(dispatch_get_main_queue(), {
[_waitingSpinner stopAnimating];
});
});
}
You need to hide it
- (IBAction)buttonScanPressed:(id)sender {
_waitingSpinner.hidden = NO;
[_waitingSpinner startAnimating];
SensBoxServiceLib *service = [[SensBoxServiceLib alloc]init];
NSString *tmp=[NSString stringWithFormat:#"%d",[service scanForSensBoxes]];
[_waitingSpinner stopAnimating];
_waitingSpinner.hidden = YES;
_sensBoxCount.text=tmp;
}

Objective c - adding threading to UIView animation method results in only one animation running

I've written a method, sortAndSlideHandCards(), that moves 6 UIButtons. Each UIButton is moved to the same position. This is done via a for-each loop and the animateWithDuration method being called on each UIButton.
This method is called for a number of players at the same time. Currently the behaviour results in UIButtons from each player moving but only one at a time. No more than one UIButton can move at any time, as if each animation is waiting for whatever animation that is currently running to stop before attempting it's own animation, essentially the code is executed sequentially for each player/UIButton. I hoped threading would help me fix this.
when I added the threading code:
backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
sortAndSlideHandCardsGroup = dispatch_group_create();
for(Player* player in _playersArray) {
dispatch_group_async(sortAndSlideHandCardsGroup, backgroundQueue, ^(void) {
[player sortAndSlideHandCards];
});
dispatch_group_wait(sortAndSlideHandCardsGroup,DISPATCH_TIME_FOREVER);
I found that only the first UIButton animation is triggered for each player and that the code gets held up in the runloop "while" because "_animationEnd" never gets set as it would appear the second animation never gets going.
I can see the method launching in its own thread
- (void) sortAndSlideHandCards {
NSLog(#"PLAYER:sortAndSlideHandCards");
CGPoint newCenter;
Card* tempCard = nil;
int count = 1;
float duration = 0.2 / _speedMultiplyer;
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
for(Card *card in _handCards) { //move cards in hand to one postion in hand
if(count == 1) {
tempCard = [[Card alloc] init:_screenWidth:_screenHeight :[card getNumber] :[card getCardWeight] :[card getSuit] :[card getIsSpecial]];
[tempCard setImageSrc: _playerNumber :!_isPlayerOnPhone :count : true :_view: _isAI: [_handCards count]];
newCenter = [tempCard getButton].center;
}
_animationStillRunning = true;
if(![[DealViewController getCardsInPlayArray] containsObject:card] ) {
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionLayoutSubviews animations:^{[card getButton].center = newCenter;} completion:^(BOOL finished){[self animationEnd];}];
while (_animationStillRunning){ //endAnimation will set _animationStillRunning to false when called
//stuck in here after first UIButton when threading code is in play
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
} //endAnimation will set _animationStillRunning to false when called
}
count++;
}
}
When i comment out the threading code each UIButton (Card) will animate one after another.
With the threading code is in play the first UIButton will animate but during the second run through the for-loop, the code will be stuck in the while-loop, waiting for the animation to end. I'm guessing the second animation doesn't even start.
I also tried this for the threading code:
[player performSelectorInBackground:#selector(sortAndSlideHandCards) withObject:nil];
Same outcome
Anyone have any ideas why animateWithDuration doesn't like getting called in a loop when in a thread other than the main one?
You should just be able to kick off the animations you want from whatever UI action is performed. The UIView animateWith... methods return immediately, so you don't need to worry about waiting for them to complete.
If you have an unknown number of animations to kick off sequentially, use the delay parameter. Pseudocode:
NSTimeInterval delay = 0;
NSTimeInterval duration = 0.25;
for (UIView *view in viewsToAnimate)
{
[UIView animateWithDuration:duration delay:delay animations:^{ ... animations ...}];
delay += duration;
}
This will increase the delay for each successive animation, so it starts at the end of the previous one.

How to use UIActivityIndicatorView on custom UIButton?

I want to use UIActivityIndicatorView on my custom UIButton.
Here is my code:
if (sender.tag == 1)
{
// Start animating
activityIndicator.hidden = NO;
[activityIndicator startAnimating];
// Check if the network is available
if ([self reachable]) {
// Stop animating
activityIndicator.hidden = YES;
[activityIndicator stopAnimating];
}
}
What I want to do here is:
Once the user touch the button, I want to start the ActivityIndicatior while Reachable checking the network availability. Once it done pass it to the next view.
Update
UIActivityIndicator is on top of my custom UIButton. It build successfully, but ActivityIndicator is not showing when I touch the button.
I'm sure you had your answer since then.
After 4 years, language has changed to Swift, but I had the same problem : UIActivityIndicatorView is not showing when I add it through :
self.myButton.addSubview(self.myIndicatorView)
I had to climb a level up :
self.myButton.superview!.addSubview(self.myIndicatorView)
Assuming everything else is correct in your project, the problem here is that you're showing and hiding an activity indicator in the same event loop, without giving a pause to draw it. Let me explain:
If you have the code:
UIView* view = someView;
view.backgroundColor = [UIColor redColor];
// various synchronous operations
view.backgroundColor = [UIColor yellowColor];
The view will only ever have a background color of yellow.
To answer your question, you probably want to animate the spinner, do some asynchronous operation, then stop the animation. The key being to not stall on the main event loop while your asynchronous task is running. If your comfortable with blocks it would look something like this:
if (sender.tag == 1) {
// Start animating
activityIndicator.hidden = NO;
[activityIndicator startAnimating];
// Check if the network is available
[self checkReachableWithCallback:^{
// Stop animating
activityIndicator.hidden = YES;
[activityIndicator stopAnimating];
}];
}

How to correctly display a "progress" sheet modally while using Grand Central Dispatch to process something?

I'm trying to display a sheet on a window containing a single progress bar, to show the progress of some long function running asynchronously using Grand Central Dispatch. I've almost got it, but can't get the sheet to appear to be in focus, probably because I haven't used runModalForWindow: or similar.
This is approximately what I'm doing at the moment, it happens as a result of a button press on the main window:
// Prepare sheet and show it...
[NSApp beginSheet:progressSheet modalForWindow:window modalDelegate:nil didEndSelector:NULL contextInfo:NULL];
[progressSheet makeKeyAndOrderFront:self];
[progressBar setIndeterminate:NO];
[progressBar setDoubleValue:0.f];
[progressBar startAnimation:self];
// Start computation using GCD...
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 1000; i ++) {
// Do some large computation here
// ...
// Update the progress bar which is in the sheet:
dispatch_async(dispatch_get_main_queue(), ^{
[progressBar setDoubleValue:(double)i];
});
}
// Calculation finished, remove sheet on main thread
dispatch_async(dispatch_get_main_queue(), ^{
[progressBar setIndeterminate:YES];
[NSApp endSheet:progressSheet];
[progressSheet orderOut:self];
});
});
This works, except the main window is still in focus, the sheet is out of focus, and the progress bar doesn't animate (unless I use setUsesThreadedAnimation:YES on it).
The problem I think I'm having is that I'm not sure how to run the sheet modally without blocking the main thread before I start the asynchronous computation?
As stated by Brad, it should work.
To do a quick test, I created a sheet programmatically (normally, you would probably use a nib file, but they are hard to paste into this text). If I call the code below from a button in a normal Cocoa window, it works as expected. Notice that the text field on the sheet is first responder, and if you type on the keyboard while it is open, it will accept the input.
#define maxloop 1000
- (IBAction)startTask:(id)sender
{
// Prepare sheet and show it...
breakLoop = NO;
NSRect sheetRect = NSMakeRect(0, 0, 400, 114);
NSWindow *progSheet = [[NSWindow alloc] initWithContentRect:sheetRect
styleMask:NSTitledWindowMask
backing:NSBackingStoreBuffered
defer:YES];
NSView *contentView = [[NSView alloc] initWithFrame:sheetRect];
NSProgressIndicator *progInd = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(143, 74, 239, 20)];
NSTextField *inputField = [[NSTextField alloc] initWithFrame:NSMakeRect(145, 48, 235, 22)];
NSButton *cancelButton = [[NSButton alloc] initWithFrame:NSMakeRect(304, 12, 82, 32)];
cancelButton.bezelStyle = NSRoundedBezelStyle;
cancelButton.title = #"Cancel";
cancelButton.action = #selector(cancelTask:);
cancelButton.target = self;
[contentView addSubview:progInd];
[contentView addSubview:inputField];
[contentView addSubview:cancelButton];
[progSheet setContentView:contentView];
[NSApp beginSheet:progSheet
modalForWindow:self.window
modalDelegate:nil
didEndSelector:NULL
contextInfo:NULL];
[progSheet makeKeyAndOrderFront:self];
[progInd setIndeterminate:NO];
[progInd setDoubleValue:0.f];
[progInd startAnimation:self];
// Start computation using GCD...
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < maxloop; i++) {
[NSThread sleepForTimeInterval:0.01];
if (breakLoop)
{
break;
}
// Update the progress bar which is in the sheet:
dispatch_async(dispatch_get_main_queue(), ^{
[progInd setDoubleValue: (double)i/maxloop * 100];
});
}
// Calculation finished, remove sheet on main thread
dispatch_async(dispatch_get_main_queue(), ^{
[progInd setIndeterminate:YES];
[NSApp endSheet:progSheet];
[progSheet orderOut:self];
});
});
}
- (IBAction)cancelTask:(id)sender
{
NSLog(#"Cancelling");
breakLoop = YES;
}
Apologies for the ugly sheet, but apart from that this code works as expected, so the issue you are seeing is probably unrelated to GCD.
I had exactly the same problem. With some trial and error, I found the solution. Make sure your sheet's window is (a) an NSWindow not an NSPanel (this may not matter) and that the window has a Title Bar (which, as it's a sheet you're using) will not be displayed.
I turned the Title Bar off for that reason, but somehow it's required to correctly achieve focus. Ticking the Title Bar checkbox gives my progress bar sheet focus.