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
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 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
I am recreating the UINavigationController Push animation using the new iOS 7 custom transitioning APIs.
I got the view pushing and popping fine using animationControllerForOperation
I added a edge gesture recogniser for the interactive pop gesture.
I used a UIPercentDrivenInteractiveTransition subclass and integrated code from WWDC 2013 218 - Custom Transitions Using View Controllers
It looks like it removes the fromViewController by mistake, but I don't know why.
The steps are:
Interactive pop starts
Finger is lifted after a short distance - red screenshot
The view animates back a short distance.
Red view is removed (I think) - black screenshot.
The full code is on GitHub, but here are 2 parts which I guess are important.
Gesture delegate
- (void)didSwipeBack:(UIScreenEdgePanGestureRecognizer *)edgePanGestureRecognizer {
if (state == UIGestureRecognizerStateBegan) {
self.isInteractive = YES;
[self.parentNavigationController popViewControllerAnimated:YES];
}
if (!self.isInteractive) return;
switch (state)
{
case UIGestureRecognizerStateChanged: {
// Calculate percentage ...
[self updateInteractiveTransition:percentagePanned];
break;
}
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateEnded: {
if (state != UIGestureRecognizerStateCancelled &&
isPannedMoreThanHalfWay) {
[self finishInteractiveTransition];
} else {
[self cancelInteractiveTransition];
}
self.isInteractive = NO;
break;
}
}
}
UIViewControllerAnimatedTransitioning protocol
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
// Grab views ...
[[transitionContext containerView] addSubview:toViewController.view];
// Calculate initial and final frames
toViewController.view.frame = initalToViewControllerFrame;
fromViewController.view.frame = initialFromViewControllerFrame;
[UIView animateWithDuration:RSTransitionVendorAnimationDuration delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
toViewController.view.frame = finalToViewControllerFrame;
fromViewController.view.frame = finalFromViewControllerFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
Anyone know why the screen is blank? Or Can anyone point me to some sample code. Apple don't appear have any sample code for interactive transitions using the percent driven interactions.
The first issue was a bug in the Apple sample code that I copied. The completeTransition method should have a more intelligent BOOL parameter like this:
[UIView animateWithDuration:RSTransitionVendorAnimationDuration delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
toViewController.view.frame = finalToViewControllerFrame;
fromViewController.view.frame = finalFromViewControllerFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
Thanks to #rounak for pointing me to the objc.io post.
This then presented another issue relating to the animation. The animation would stop, present a blank view and them carry on. This was almost defiantly a UIKit bug. The fix was to set the completionSpeed to 0.99 instead of 1.0. The default value is 1.0 so I guess that setting it to this doesn't do some side effect in their custom setter.
// self is a UIPercentDrivenInteractiveTransition
self.completionSpeed = 0.99;
I don't think you need a UIPercentDrivenInteractiveTransition subclass. I just create a new UIPercentDrivenInteractiveTransition object, hold a strong reference to it and return it in interactionControllerForAnimationController method.
This link for interactive transitions is quite helpful.
I'm building a swipeable cell adding a pan gesture to that cell. Basically, it has the same look and feel as the cells in Mailbox app, where you have a top view which you can swipe to the left or right to show another view (revealView) underneath.
I wanted to build this with the reactive approach so the way I am doing it is:
First, when I setup the view and the pan gesture, I'm filtering the rac_gestureSignal to get the current state of the gesture and update the top view position with bindings (some implementation details are simplified here) as well as hiding/showing the revealView when the gesture is ended/cancelled. I also call setNeedsLayout when either panDirection or revealView change (in order to update revealView frame accordingly) merging the signals from their values, as well as remove the reveal view on cell reusing:
- (void)setupView
{
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:nil];
panGesture.delegate = self;
RACSignal *gestureSignal = [panGesture rac_gestureSignal],
*beganOrChangeSignal = [gestureSignal filter:^BOOL(UIGestureRecognizer *gesture) {
return gesture.state == UIGestureRecognizerStateChanged || gesture.state == UIGestureRecognizerStateBegan;
}],
*endOrCancelSignal = [gestureSignal filter:^BOOL(UIGestureRecognizer *gesture) {
return gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerStateCancelled;
}];
RAC(self, contentSnapshotView.center) = [beganOrChangedSignal map:^id(id value) {
return [NSValue valueWithCGPoint:[self centerPointForTranslation:[panGesture translationInView:self]]];
}];
[beganOrChangeSignal subscribeNext:^(UIPanGestureRecognizer *panGesture) {
[self updateTopViewFrame];
[panGesture setTranslation:CGPointZero inView:self];
}];
[[endOrCancelSignal filter:^BOOL(UIPanGestureRecognizer *gestureRecognizer) {
return [self shouldShowRevealView];
}] subscribeNext:^(id x) {
[self showRevealViewAnimated:YES];
}];
[[endOrCancelSignal filter:^BOOL(UIPanGestureRecognizer *gestureRecognizer) {
return [self shouldHideRevealView];
}] subscribeNext:^(id x) {
[self hideRevealViewAnimated:YES];
}];
[[RACSignal merge:#[RACObserve(self, panDirection), RACObserve(self, revealView)]] subscribeNext:^(id x) {
[self setNeedsLayout];
}];
[[self rac_prepareForReuseSignal] subscribeNext:^(id x) {
[self.revealView removeFromSuperview];
self.revealView = nil;
}];
[self addGestureRecognizer:panGesture];
}
Then, I'm exposing a signal property (revealViewSignal) which will send YES/NO values when the reveal view shows/hides. Thus, you can subscribe to this signal and consequently act when the view changes his state. Internally, this signal will be a RACSubject sending next events after each show/hide animation ends:
- (void)showRevealViewAnimated:(BOOL)animated
{
[UIView animateWithDuration:animated ? 0.1 : 0.0
animations:^{
// SHOW ANIMATIONS
}
completion:^(BOOL finished) {
[(RACSubject *)self.revealViewSignal sendNext:#(YES)];
}];
}
- (void)hideRevealViewAnimated:(BOOL)animated
{
[UIView animateWithDuration:animated ? 0.1 : 0.0
animations:^{
// HIDE ANIMATIONS
}
completion:^(BOOL finished) {
[(RACSubject *)self.revealViewSignal sendNext:#(NO)];
}];
}
Everything works as expected but I was just wondering if this is a correct approach to build this kind of view in a RAC way. Also, there are two gesture recognizer delegate methods that I would love to setup in the same setup method above, but I wasn't able to figure out whether it's possible to use the rac_signalForSelector:fromProtocol: method here, so I ended up implementing them as always:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
return [self checkIfGestureShouldBegin:gestureRecognizer];
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return [self checkIfGestureShouldRecognize:gestureRecognizer];
}
Any help would be highly appreaciated, thanks!
Unfortunately there's currently no way to use RAC to implement a protocol method that returns a value.
It's a tricky problem since signals aren't required to send values synchronously, but obviously you need to return something when the delegate method is called. You probably don't want to block on the signal because it'd be easy to dead or live lock.
I'm implementing a custom interactive collection view layout transition, and would like to configure it so that on calling finishInteractiveTransition with a transition layout transitionProgress of 1.0, the completion block of startInteractiveTransitionToCollectionViewLayout:completion: is fired immediately.
I require this behaviour as I would like to be able to immediately start another transition after this one has finished. With this completion block not firing immediately, I end up with nested push animation can result in corrupted navigation bar in the log, since the transition context hasn't had its completeTransition: method called yet.
As it is, even when there is no animation required, there is a small delay of about 0.05s. Is it possible to force UICollectionView to not animate this completion?
Some code...
I have a transition controller that handles both animated & interactive transitions for a navigation transition. The relevant parts of that controller are as follows:
- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
self.context = transitionContext;
UIView *inView = [self.context containerView];
UICollectionViewController *fromCVC = (UICollectionViewController *)[self.context viewControllerForKey:UITransitionContextFromViewControllerKey];
UICollectionViewController *toCVC = (UICollectionViewController *)[self.context viewControllerForKey:UITransitionContextToViewControllerKey];
self.transitionLayout = (MyTransitionLayout *)[fromCVC.collectionView startInteractiveTransitionToCollectionViewLayout:toCVC.collectionViewLayout completion:^(BOOL completed, BOOL finish) {
[self.context completeTransition:completed];
[inView addSubview:toCVC.view];
}];
}
- (void)updateInteractiveTransition:(CGFloat)progress
{
if (!self.context) return;
self.percentComplete = progress;
if (self.percentComplete != self.transitionLayout.transitionProgress) {
self.transitionLayout.transitionProgress = self.percentComplete;
[self.transitionLayout invalidateLayout];
[self.context updateInteractiveTransition:self.percentComplete];
}
}
- (void)finishInteractiveTransition
{
if (!self.context) return;
UICollectionViewController *fromCVC = (UICollectionViewController *)[self.context viewControllerForKey:UITransitionContextFromViewControllerKey];
[fromCVC.collectionView finishInteractiveTransition];
[self.context finishInteractiveTransition];
}
- (void)cancelInteractiveTransition
{
if (!self.context) return;
UICollectionViewController *fromCVC = (UICollectionViewController *)[self.context viewControllerForKey:UITransitionContextFromViewControllerKey];
[fromCVC.collectionView cancelInteractiveTransition];
[self.context cancelInteractiveTransition];
}
I've verified that on calling finishInteractiveTransition the transition layout's transitionProgress is exactly 1.0. According to the UIViewControllerTransitioning.h header file, the completionSpeed defaults to 1.0, resulting in a completion duration of (1 - percentComplete)*duration, so the duration should be 0, but it's not... it's taking at least a couple of run loops before the completion block is called.
Is it possible to do what I want, or will I have to implement my own version of startInteractiveTransitionToCollectionViewLayout:completion:? (not the end of the world, but I'd rather stick to the standard APIs where possible...)