I have a custom animation wich i am calling with this method:
- (void)spinWithOptions:(UIViewAnimationOptions)options directionForward:(BOOL)directionForward {
[UIView animateWithDuration:0.3
delay:0.0
options:options
animations:^{
CATransform3D transform;
if (!directionForward) {
transform = CATransform3DIdentity;
} else {
transform = CATransform3DIdentity;
transform.m34 = -1.0 / 500.0;
transform = CATransform3DRotate(transform, M_PI - 0.0001f /*full rotation*/, 0, 1, 0.0f);
}
self.logoImageView.layer.transform = transform;
} completion:^(BOOL finished) {
if (!_animating && options != UIViewAnimationOptionCurveEaseOut) {
[self spinWithOptions:UIViewAnimationOptionCurveEaseOut directionForward:NO];
} else if (options == UIViewAnimationOptionCurveLinear) {
[self spinWithOptions:UIViewAnimationOptionCurveLinear directionForward:!directionForward];
} else {
// animation finished
}
}];
}
but I have a problem, when the animation runs and I do some processing from a server with AFNetworking and CoreData the animation freezes, i think the main thread is blocked but I also have a MBProgresHUD and that doesn't freeze. Any idea how i can make this animation not freeze?
you do the animation and all the networking on the same thread. only one thing can run on one thread at a time though so your loading blocks the animation.
you need to offload tasks so the thread can run just one thing. ANY ui modification needs to happen on the main thread, so we offload the networking.
- startMyLongMethod {
[self startAnimation]; //your spin thingy
//get a background thread from GCD and do the work there
dispatch_async(dispatch_get_global_queue(0,0), ^{
//do longRunning op (afnetwork and json parsing!)
id result = [self doWork];
//go back to the main thread and stop the animation
dispatch_async(dispatch_get_main_queue(), ^{
[self updateUI:result];
[self stopAnimation];//your spin thingy
});
});
}
note: example code and written inline! The approach should be clear now though
Related
I am having some trouble handling animations in some objective C code.
First, here is the relevant code:
BOOL pauseFlag; // Instance variable.
CGFloat animationDuration,pauseDuration; // Instance variables.
......
pauseFlag = NO;
animationDuration = 1.0;
pauseDuration = 1.0;
- (void)animationFunction
{
[UIView animateWithDuration:animationDuration
delay:pauseFlag?pauseDuration:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
......
}
completion:^(BOOL finished){
......
pauseFlag = Some_New_Value;
[self animationFunction];
}];
}
Then here is the problem:
The delay supposed to take place when pauseFlag is YES is not happening.
Of course, before writing this post I have tried various solutions which came up to my mind, like changing the options, and I also checked that when entering animationFunction pauseFlag had the value YES. But in all cases the delay was ignored.
What did I do wrong? I need to insert a pause in my animation and thought this was the simplest way to do it.
Anyone has an idea?
Just for information, beside this issue. This animation code is working fine.
Try to animate your view with UIViewPropertyAnimator:
UIViewPropertyAnimator* animator = [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:animationDuration
delay:pauseFlag?pauseDuration:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
......
}
completion:^(UIViewAnimatingPosition finalPosition) {
......
pauseFlag = Some_New_Value;
[self animationFunction];
}];
If you want to pause the animation call pauseAnimation:
[animator pauseAnimation];
To resume it call startAnimation:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[animator startAnimation];
});
All code in this post was tested in Xcode 10.2.1.
I want to show loading animation while I am doing some initialisation. So I want to spin my (UIImageView*)_LogoImage until my initialisation will be completed. Then, after initialisation will be finished, I want to scale my _LogoImage. So, all this things begins from viewDidAppear, where I am calling beginLoading: method.
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[self beginLoading];
}
Here, I am starting my spin animation. Assume, I am doing some initialisation in the next two code lines, I changed them to make a thread sleep to make a similar behaviour of my initialisation code. Then I am calling stopSpin: method to make the next half circle and to do my last scaling animation.
-(void)beginLoading{
[self startSpin];
[self sleepAction:3.0f];
[self sleepAction:1.0f];
[self stopSpin];
}
-(void)sleepAction:(float)sleepTime{
NSLog(#"SleepTime:[%f]",sleepTime);
[NSThread sleepForTimeInterval:sleepTime];
}
Here's my spinning code, which I am calling recursively until my BOOL refreshAnimating is Equal to YES. if not - running the last scaling animation
-(void)startSpin{
[UIView animateWithDuration:0.4
delay:0
options:UIViewAnimationOptionCurveLinear
animations:^{
_LogoImage.transform = CGAffineTransformMakeRotation(M_PI);
}
completion:^(BOOL finished){
[UIView animateWithDuration:0.4
delay:0
options:UIViewAnimationOptionCurveLinear
animations:^{
_LogoImage.transform = CGAffineTransformMakeRotation(0);
}
completion:^(BOOL finished){
if (refreshAnimating) {
[self startSpin];
}else{
[self refreshFinished];
}
}];
}];
}
-(void)stopSpin{
refreshAnimating = NO;
}
-(void)refreshFinished{
[UIView animateWithDuration:0.4 animations:^{
[_LogoImage setTransform:CGAffineTransformMakeScale(2, 2)];
}];
}
The problem is that my animation is not running, until the last initialisation code line completes. And after completion - I can see only the last animation. I was trying to put some code performing in the background and main thread, but I didn't run the way I want.
The rotation animation should start when the view appears, and continue until my initialisation code complete - then I want to see my last scale animation, which will indicate that my initialisation is completed.
Please, help.
All animations run on main thread and if you use sleep, it blocks your animations(main thread). Instead you can use a library like: MBProgressHUD please feel free to ask further questions.
Edit
-(void)startSpin{
_LogoImage.transform = CGAffineTransformMakeRotation(M_PI);
[UIView animateWithDuration:0.4
delay:0
options:UIViewAnimationOptionCurveLinear
animations:^{
_LogoImage.transform =CGAffineTransformMakeRotation(0);
}
completion:nil];
}
Can you try this. I think that is what are trying to do.
As #Ersin Sezgin said, all animations run on main thread.
So now, I am doing initialisation in background with completion block handler, which is forcing stopSpin: after initialisation completes.
Here's some code block. For better readability there's typedef of block.
typedef void(^OperationSuccessCompletionHandler)(BOOL success);
in .m file
-(void)sleepAction:(OperationSuccessCompletionHandler)success{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// We are doing initialisation here
BOOL isOperationSuccess = YES // Assume initialisation was successful
success(isOperationSuccess);
});
}
So now we can do this in another way
-(void)beginLoading{
[self startSpin];
[self sleepAction:^(BOOL success){
[self sleepAction:^(BOOL success){
[self stopSpin];
}];
}];
}
I'm using an interactive custom push transition with a UIPercentDrivenInteractiveTransition. The gesture recognizer successfully calls the interaction controller's updateInteractiveTransition. Likewise, the animation successfully completes when I call the interaction controller's finishInteractiveTransition.
But, sometimes I get a extra bit of distracting animation at the end (where it seems to repeat the latter part of the animation). With reasonably simple animations, I rarely see this symptom on the iPhone 5 (though I routinely see it on the simulator when working on slow laptop). If I make the animation more computationally expensive (e.g. lots of shadows, multiple views animating different directions, etc.), the frequency of this problem on the device increases.
Has anyone else seen this problem and figured out a solution other than streamlining the animations (which I admittedly should do anyway) and/or writing my own interaction controllers? The UIPercentDrivenInteractiveTransition approach has a certain elegance to it, but I'm uneasy with the fact that it misbehaves non-deterministically. Have others seen this behavior? Does anyone know of other solutions?
To illustrate the effect, see the image below. Notice how the second scene, the red view, when the animation finishes, seems to repeat the latter part of its animation a second time.
This animation is generated by:
repeatedly calling updateInteractiveTransition, progressing update from 0% to 40%;
momentarily pausing (so you can differentiate between the interactive transition and the completion animation resulting from finishInteractiveTransition);
then calling finishInteractiveTransition to complete the animation; and
the animation controller's animation's completion block calls completeTransition for the transitionContext, in order to clean everything up.
Doing some diagnostics, it appears that it is this last step that triggers that extraneous bit of animation. The animation controller's completion block is called when the animation is finished, but as soon as I call completeTransition, it sometimes repeats the last bit of the animation (notably when using complex animations).
I don't think it's relevant, but this is my code for configuring the navigation controller to perform interactive transitions:
- (void)viewDidLoad
{
[super viewDidLoad];
self.navigationController.delegate = self;
self.interationController = [[UIPercentDrivenInteractiveTransition alloc] init];
}
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController*)fromVC
toViewController:(UIViewController*)toVC
{
if (operation == UINavigationControllerOperationPush)
return [[PushAnimator alloc] init];
return nil;
}
- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController*)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>)animationController
{
return self.interationController;
}
My PushAnimator is:
#implementation PushAnimator
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
return 5.0;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
[[transitionContext containerView] addSubview:toViewController.view];
toViewController.view.frame = CGRectOffset(fromViewController.view.frame, fromViewController.view.frame.size.width, 0);;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toViewController.view.frame = fromViewController.view.frame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
#end
Note, when I put logging statement where I call completeTransition, I can see that this extraneous bit of animation happens after I call completeTransition (even though the animation was really done at that point). This would suggest that that extra animation may have been a result of the call to completeTransition.
FYI, I've done this experiment with a gesture recognizer:
- (void)handlePan:(UIScreenEdgePanGestureRecognizer *)gesture
{
CGFloat width = gesture.view.frame.size.width;
if (gesture.state == UIGestureRecognizerStateBegan) {
[self performSegueWithIdentifier:#"pushToSecond" sender:self];
} else if (gesture.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [gesture translationInView:gesture.view];
[self.interactionController updateInteractiveTransition:ABS(translation.x / width)];
} else if (gesture.state == UIGestureRecognizerStateEnded ||
gesture.state == UIGestureRecognizerStateCancelled)
{
CGPoint translation = [gesture translationInView:gesture.view];
CGPoint velocity = [gesture velocityInView:gesture.view];
CGFloat percent = ABS(translation.x + velocity.x * 0.25 / width);
if (percent < 0.5 || gesture.state == UIGestureRecognizerStateCancelled) {
[self.interactionController cancelInteractiveTransition];
} else {
[self.interactionController finishInteractiveTransition];
}
}
}
I also did it by calling the updateInteractiveTransition and finishInteractiveTransition manually (eliminating the gesture recognizer from the equation), and it still exhibits this strange behavior:
[self performSegueWithIdentifier:#"pushToSecond" sender:self];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.interactionController updateInteractiveTransition:0.40];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.interactionController finishInteractiveTransition];
});
});
Bottom line, I've concluded that this is a problem isolated to UIPercentDrivenInteractiveTransition with complex animations. I can minimize the problem by simplifying them (e.g. snapshotting and animated snapshotted views). I also suspect I could solve this by not using UIPercentDrivenInteractiveTransition and writing my own interaction controller, which would do the animation itself, without trying to interpolate the animationWithDuration block.
But I was wondering if anyone has figured out any other tricks to using UIPercentDrivenInteractiveTransition with complex animations.
This problem arises only in simulator.
SOLUTION: self.interactiveAnimator.completionSpeed = 0.999;
bug reported here: http://openradar.appspot.com/14675246
I've seen something similar. I have two possible workarounds. One is to use delayed performance in the animation completion handler:
} completion:^(BOOL finished) {
double delayInSeconds = 0.1;
dispatch_time_t popTime =
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
BOOL cancelled = [transitionContext transitionWasCancelled];
[transitionContext completeTransition:!cancelled];
});
self.interacting = NO;
}];
The other possibility is: don't use percent-drive animation! I've never had a problem like this when driving the interactive custom animation myself manually.
The reason for this error in my case was setting the frame of the view being animated multiple times. I'm only setting the view frame ONCE and it fixed my issues.
So in this case, the frame of "toViewController.view" was set TWICE, thus making the animation have an unwanted behavior
Why does the following unit test pass using Xcode 5.0 and XCTesting? I mean, I understand the bottom line: 1 == 0 is not evaluated. But why is it not evaluated? How can I make this perform so that it would fail?
- (void)testAnimationResult
{
[UIView animateWithDuration:1.5 animations:^{
// Some animation
} completion:^(BOOL finished) {
XCTAssertTrue(1 == 0, #"Error: 1 does not equal 0, of course!");
}];
}
This will, technically, work. But of course the test will sit and run for 2 seconds. If you have a few thousand tests, this can add up.
More effective is to stub out the UIView static method in a category so that it takes effect immediately. Then include that file in your test target (but not your app target) so that the category is compiled into your tests only. We use:
#import "UIView+Spec.h"
#implementation UIView (Spec)
#pragma mark - Animation
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion {
if (animations) {
animations();
}
if (completion) {
completion(YES);
}
}
#end
The above simply executes the animation block immediately, and the completion block immediately if it's provided as well.
Better approach (right now) is to use expectation.
func testAnimationResult() {
let exp = expectation(description: "Wait for animation")
UIView.animate(withDuration: 0.5, animations: {
// Some animation
}) { finished in
XCTAssertTrue(1 == 0)
exp.fulfill()
}
waitForExpectations(timeout: 1.0, handler: nil)
}
#dasblinkenlight was on the right track; this is what I did to make it work correctly:
- (void)testAnimationResult
{
[UIView animateWithDuration:1.5 animations:^{
// Some animation
} completion:^(BOOL finished) {
XCTAssertTrue(1 == 0, #"Error: 1 does not equal 0, of course!");
}];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];
}
The goal here is to convert an asynchronous operation into a synchronous operation. The most general way to do this is with a semaphore.
- (void)testAnimationResult {
// Create a semaphore object
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
[UIView animateWithDuration:1.5 animations:^{
// Some animation
} completion:^(BOOL finished) {
// Signal the operation is complete
dispatch_semaphore_signal(sem);
}];
// Wait for the operation to complete, but not forever
double delayInSeconds = 5.0; // How long until it's too long?
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
long timeoutResult = dispatch_semaphore_wait(sem, timeout);
// Perform any tests (these could also go in the completion block,
// but it's usually clearer to put them here.
XCTAssertTrue(timeoutResult == 0, #"Semaphore timed out without completing.");
XCTAssertTrue(1 == 0, #"Error: 1 does not equal 0, of course!");
}
If you want to see some examples of this in action, look at RNCryptorTests.
I can call some selector by this code
[self performSelector:<#(SEL)#> onThread:<#(NSThread *)#> withObject:<#(id)#> waitUntilDone:<#(BOOL)#>]
and the Thread will wait until this selector is working (if I set waitUntilDone:YES). My question is - Can i wait until some code is done?
for example. I have some subview, which loads with animation. Animation duration is 2 sec. And if i make some actions on background view, while animating, i have an error. I'd like to wait until animation is over. And I can't to make a selector, and use
[self performSelector:<#(SEL)#> onThread:<#(NSThread *)#> withObject:<#(id)#> waitUntilDone:<#(BOOL)#>]
Any suggestions? )
You can try this way
-(void)waitUntilDone:(void(^)(void))waitBlock {
//use your statement or call method here
if(waitBlock){
waitBlock();
}
}
And you need to use it as
[self.tableView waitUntilDone:^{
//call the required method here
}];
UIView animation gives this to you in the form of the block style animation
[UIView animateWithDuration:0.25f
animations:^{
// animations
} completion:^(BOOL finished) {
// run code when the animation is finished
}];
Wait until animation gets finished & then do rest of the things :
For eg:
[UIView animateWithDuration:duration
animations:^
{
//DO ANIMATION ADJUSTMENTS HERE
}
completion:^(BOOL finished)
{
//ANIMATION DID FINISH -- DO YOUR STUFF
}];