Mixing GCD with view animation - ios

I want to do three view animations, staggered a little. To do this I' mixing GCD and the view animation as follows...
EDIT - I'd like to do a flip animation with the children of viewA, then viewB, then viewC. I'd like these animations to overlap...
random external action causes: A-----------A'
B-----------B'
C---------C'
The trouble is, when a triggering action occurs during the A->C' animation, the states of the three get mixed up. I'd like a way of doing the three staggered actions as a single, uninterrupted action.
- (void)flipAnimated:(BOOL)animated {
// views A,B,C are initialized here
// kFLIPDURATION is 0.8
NSTimeInterval interval = (animated)? kFLIPDURATION / 2.0 : 0;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.01 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self flipChildrenOfView:viewA animated:animated];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, interval * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self flipChildrenOfView:viewB animated:animated];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2*interval * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self flipChildrenOfView:viewC animated:animated];
});
}
- (void)flipChildrenOfView:(UIView *)parent animated:(BOOL)animated {
// some setup here to get visible and hidden views
[UIView transitionFromView:visibleView
toView:hiddenView
duration:((animated)? kFLIPDURATION : 0)
options:UIViewAnimationOptionTransitionFlipFromLeft | UIViewAnimationOptionShowHideTransitionViews
completion:nil];
}
This works fine, except there are timer events and sometimes user actions driving these, and every so often, flipAnimated: is called while the animations are underway. The result is mixed up views and sadness.
What I tried so far was (1) reading and them becoming confused about GCD alternatives. (2) I tried an instance variable called animating, added a completion block to my view animation calling method, then did this...
#property(assign,nonatomic) BOOL animating;
- (void)flipAnimated:(BOOL)animated {
if (self.animating) return;
self.animating = YES;
NSTimeInterval interval = (animated)? kFLIPDURATION / 2.0 : 0;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.01 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self flipChildrenOfView:viewA animated:animated completion:nil];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, interval * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self flipChildrenOfView:viewB animated:animated completion:nil];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2*interval * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self flipChildrenOfView:viewC animated:animated completion:^(BOOL finished) {
self.animating = NO;
}];
});
}
But alas, same behavior. I could observe the block seeming to work with NSLogs, but the views would still get mixed up. (With a second flip starting before a prior one was done). I assume this is because I got in over my head on concurrency.
Would sure appreciate a helping hand, especially a code example. --- Almost forgot: what would be even better than stoping concurrent requests, would be to have it queued and then run after the running is finished.

The docs for transitionFromView:... do say "The view transition starts immediately unless another animation is already in-flight, in which case it starts immediately after the current animation finishes." So overlapping animation won't work like this.
Drop to CoreAnimation, e.g. CATransform3D, see Core animation animating the layer to flip horizontally.

#GrahamPerks asked me to clarify the question, and doing so clarified my thinking. In case this can help someone else, here's what I did.
The code was initializing the subviews to animate with the same staggered timing as the flips. When the code was re-entered, it was examining the view hierarchy in a partially changed state.
The fix was to decide about transition views up front, not during, the three step animation...
- (void)flipAnimated:(BOOL)animated {
// views A,B,C are initialized here
// FIX: but also initialize six views here
UIView *viewAVisible, *viewAHidden, *viewBVisible, *viewBHidden, *viewCVisible, *viewCHidden;
// FIX: refactor my animation wrapper to take the two views to transition
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.01 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self flipFrom:viewAHidden to:viewAVisible animated:animated];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, interval * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self flipFrom:viewBHidden to:viewBVisible animated:animated];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2*interval * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self flipFrom:viewCHidden to:viewCVisible animated:animated];
});
}
Now when a new animation request comes in concurrently, the views are in a coordinated state (because I think UIKit makes the change called for by the animation immediately).

Related

Pausing app to display message for one second before dismissing a view

I have a simple timer that on completion shows the message "Rest Time Complete" I want that message to stay on the screen for one second, then dismiss a view. I've tried a few things that I'll detail below, but I can never get the message to display, pause, then dismiss the view. The timer pauses on the last number in the timer, then the message always appears as the view is being dismissed. Here is my code:
- (void)restTimerComplete
{
self.restTimeLabel.text = #"Rest Period Complete!";
[NSThread sleepForTimeInterval:1];
[self hideRestTimerAnimated:YES];
}
I've also tried using the simple sleep(1), but that has the same end result.
The simplest approach is to use dispatch_after:
- (void)restTimerComplete {
self.restTimeLabel.text = #"Rest Period Complete!";
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self hideRestTimerAnimated:YES];
});
}

Blocking the main thread immediately in the first view cause a flash of black screen on iOS 8

Adding the following code to a new project cause a flash of a black screen. It seems to be caused by the fade out animation of the splash screen. Unfortunately, the long process must be on main thread. It is possible to avoid it by delaying it but it is unreliable and lengthens the loading process.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// A spinner is shown
// This most probably will not cause a black screen
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// [self loadSomething];
// });
// This will not cause a black screen but not suitable for my use
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// [self loadSomething];
// });
dispatch_async(dispatch_get_main_queue(), ^{
[self loadSomething];
});
}
- (void)loadSomething
{
NSLog(#"start long process");
NSDate *start = [NSDate date];
while (-[start timeIntervalSinceNow] < 5) {
}
}
Consider why it must be on the main thread and come up with a way to move this processing or minimise it.
You should create a root view controller which displays the app default image and have that view controller displayed (probably with some animation or progress indication) until your processing has completed.
If you're already on your main thread, then there is no need of getting the main thread asynchronously.
dispatch_async(dispatch_get_main_queue(), ^{
});
The above code should be removed.
In case you want to do something or load something, then you should use
performSelectorInBackground
Hope this helps...

How to use GCD to control animation sequence

I have a View to show and hide to give users some hint.
The show and hide method look some like this:
-(void)show{
[UIView animateWithDuration:3.0f
animations:^{
//do something to show self to give hint;
self.frame = CGRectMake(0,0,0,0);
} completion:nil];
}
-(void)hide{
[UIView animateWithDuration:3.0f
animations:^{
//do something to hide self to give hint;
self.frame = CGRectMake(centerX,centerY,100,100);
} completion:nil];
}
when showing a new view, I must call hide method, and then show method. But the duration delay, 3.0f, will cause some error. I was using methods like this:
dispatch_async(dispatch_get_main_queue(), ^{
[view hide];
});
dispatch_async(dispatch_get_main_queue(), ^{
[view show];
});
I was calling show method right after hide method. Animations cannot execute as the sequence they are submitted to the queue. What I want is the show method executed exactly after the hide method completed. How can I control the order of these two methods.
I think I cannot use the completion handler cause I cannot assure where these two methods are called, or whether the view is shown when I called another show method or hide method.
If I am not clear, any suggestions? I will reedit my questions.
PS:
It's not just a flash. When next show method is called, I can not assure the last view is shown or hide and how long the last view is being shown, that is, if the view is being shown and the hide method has been called and already completed, then the show method is called, the result is right. If the view is being shown, another hint view need to be presented, I will call hide first, then show, since the main_queue is serial but the animation block is executed syncly, so the result is wrong. I am looking for is there some kind of lock in GCD that can help me execute a block after last queued block is completed rather than changing within show and hide method. cause there are many other calls to show and hide method with many different kinds of parameters, I need to fix many places in my code.
If you want to execute one task at a time in the order in which they are added to the queue, Use serial queue.
So you can use a serial queue to execute show and hide task at a time in the added order. Yeah, the main queue is ok for that.
However UIView -animateWithDuration:animations: method is kind of asynchronous call, the method returns immediately. So you need to wait until the completion block was called.
If you want to wait until some tasks were finished, Use dispatch group. But you should avoid to wait like that on the main queue. It blocks the main queue. Bad app.
Thus, you might need to use a serial queue and dispatch group as the following.
properties and initialize
#property (nonatomic, strong) dispatch_queue_t serialQueue;
#property (nonatomic, strong) dispatch_group_t group;
-(void)initQueue {
// create a serial queue
self.serialQueue = dispatch_queue_create("com.example.serialQueue", 0);
// create a dispatch group
self.group = dispatch_group_create();
}
a method that uses the serial queue and the dispatch group
-(void)animateSyncWithDuration:(NSTimeInterval)duration animations:(block_t)animations {
dispatch_async(self.serialQueue, ^{
/*
* This block is invoked on the serial queue
* This block would never be executed concurrently
*/
/*
* Enter the dispatch group
*/
dispatch_group_enter(self.group);
dispatch_async(dispatch_get_main_queue(), ^{
/*
* This block is invoked on the main queue
* It is safe to use UIKit
*/
[UIView animateWithDuration:duration animations:animations completion:^{
/*
* This completion block is invoked on the main queue
* Now leave the dispatch group
*/
dispatch_group_leave(self.group);
}];
});
/*
* Wait until leaving the dispatch group from the UIView animation completion block
* It means it blocks the serial queue
*/
dispatch_group_wait(self.group, DISPATCH_TIME_FOREVER);
});
}
show and hide
-(void)show{
[self animateSyncWithDuration:3.0f animations:^{
//do something to show self to give hint;
self.frame = CGRectMake(0,0,0,0);
}];
}
-(void)hide{
[self animateSyncWithDuration:3.0f animations:^{
//do something to hide self to give hint;
self.frame = CGRectMake(centerX,centerY,100,100);
}];
}
If what you wanted is one action (hide then show itself), you should make just one animation to do this instead of join two animations.
There are two possible solutions.
(1) use animation repeat and auto-reverse (need to reset back to original size in completion callback)
-(void) flash {
CGRect bounds = self.bounds;
[UIView animateWithDuration:1.0f
delay:0.0f
options:UIViewAnimationOptionAutoreverse |
UIViewAnimationOptionRepeat
animations:^{
[UIView setAnimationRepeatCount:1];
self.bounds = CGRectZero;
}
completion:^(BOOL finished) {
self.bounds = bounds;
}];
}
(2) use key frame animation
-(void) flash2 {
[UIView animateKeyframesWithDuration:1.0f
delay:0.0f
options:UIViewKeyframeAnimationOptionCalculationModeLinear
animations:^{
CGRect bounds = self.bounds;
[UIView addKeyframeWithRelativeStartTime:0.0
relativeDuration:0.5
animations:^{ self.bounds = CGRectZero; }];
[UIView addKeyframeWithRelativeStartTime:0.5
relativeDuration:0.5
animations:^{ self.bounds = bounds; }];
}
completion:nil];
}
i am using following way to put delay in function calling.
- (void) doAnimation : (double) delay {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Call your First function here");
});
double delayInSeconds = delay;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
NSLog(#"Call your second function here");
});
}
I may not be fully understanding the use case, but what I think you should be doing here is checking whether the hide operation actually needs to occur. Also, since the hide has an animation duration of 3 seconds in your code, you should create your method with a completion block, so you can do something similar to what I've written below in pseudocode:
- (void)hideIfNeededWithCompletionBlock:((^)())completionBlock {
if (self.isShowing) {
[self hideWithCompletionBlock:^(BOOL didHide) {
if (completionBlock) {
completionBlock();
}
}];
} else {
if (completionBlock) {
//We didn't need to hide anything, so we're done
completionBlock();
}
}
}
Then you can call it like so:
[self hideIfNeededWithCompletionBlock:^(){
[self show];
}];
You can do something similar with the show method if you need that flexibility.
Also, depending on your needs, you can make your method take a BOOL for whether to animate the show/hide and if you pass NO, use a duration of 0.0.
I think its working against the UIView animations API to start wrapping it in dispatch_async blocks when you can handle it all with the API provided.

How to call a method after few seconds without affecting ui?

I am using tableview and I want to call a method after some time duration.This method return a array and reload the tableview.I want within this time duration UI doesn't stuck.
Best to use Grand Central Dispatch (GCD). If you call dispatch_after(), you can run a block of code whenever you want.
// Run this code after 1 second.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self someMethod];
});

Action Button calls multiple methods, how can i insert a short pause between them?

I have an iOS Xcode question I'm hoping someone can help me out with. I have a simple action button that invokes a series of methods to run, however these methods all write to a stream and retrieve the input that comes in return, so I'm having a hard time reading the stream and extracting the information.
I think this is because its all happening too fast. I would like it where i press the button and method one runs, waits half a second (for example), then method two, then method three, etc... can someone show me a simple code to do so please?
Thanks in advance, example below:
Chuck
- (IBAction)updateStatsButton:(id)sender {
[self method1];
[self method2];
[self method3];
self.label1.text = result from method 1;
self.label2.text = result from method 2;
self.label3.text = result from method 3;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self method1];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self method2];
});
[self performSelector:#selector(method1) withObject:nil afterDelay:2];

Resources