I present a view controller modally in my application. I'd like for the user to be able to "flick" the view away with a gesture. I wrote the code below for that:
- (void)handlePan:(UIPanGestureRecognizer *)recognizer {
CGFloat elasticThreshold = 100;
CGFloat dismissThreshold = 200;
CGPoint translation = [recognizer translationInView:self.view];
CGFloat newY = 0;
CGFloat translationFactor = 0.5;
if (recognizer.state == UIGestureRecognizerStateEnded) {
if (translation.y < dismissThreshold) {
newY = 0;
}
} else {
if (translation.y > elasticThreshold) {
CGFloat frictionLength = translation.y - elasticThreshold;
CGFloat frictionTranslation = 30 * atan(frictionLength/120) + frictionLength/10;
newY = frictionTranslation + (elasticThreshold * translationFactor);
} else {
newY = translation.y*translationFactor;
}
}
if (translation.y > dismissThreshold) {
[UIView animateKeyframesWithDuration:0.5 delay:0.0 options:0 animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.5 animations:^{
self.overlay.effect = nil;
self.collectionView.transform = CGAffineTransformMakeTranslation(0, self.view.frame.size.height);
}];
[UIView addKeyframeWithRelativeStartTime:0.1 relativeDuration:0.1 animations:^{
self.pageControl.frame = CGRectMake((self.view.frame.size.width-200)/2, self.view.frame.size.height, 200, 20);
}];
} completion:^(BOOL finished) {
if (finished) {
[self dismissViewControllerAnimated:YES completion:nil];
}
}];
} else {
self.collectionView.transform = CGAffineTransformMakeTranslation(0, newY);
self.pageControl.transform = CGAffineTransformMakeTranslation(0, (newY+self.collectionView.frame.size.height)-20);
}
}
This is hooked up to a UIGestureRecognizer:
self.pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
self.pan.delegate = self;
self.pan.maximumNumberOfTouches = 1;
[self.view addGestureRecognizer:self.pan];
However, what happens is that the completion block executes immediately. So you'll see the view move down (because of dismissViewControllerAnimated) and at the same time see the overlay.effect go away. However, what I would like is for my animations to happen and then for the view controller to dismiss itself silently.
Any ideas what's going wrong here?
This occurs because you are nesting UIView animation blocks. You should use dispatch_after for this:
double delayInSeconds = 0.5;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self dismissViewControllerAnimated:YES completion:nil];
});
A UIView keyframe (or standard) animation will execute its completion block immediately if there is no work to be done. It won't wait for the duration of the animation if there is nothing to animate.
It seems that in your case when you are kicking off the dismiss animation, the views are already in their final state, hence there is no animation and the completion block is run immediately.
Related
I'm developing an IOS app using objective-c. I came across with an instability problem when animating a view's constraints. The animation works well regarding the final position of the frame, but randomly, the trailing(right) constraint is omitted for the first animation block(left constraint never reach to -20 constant value), however leading(left), top and bottom constraints are working as expected.
Please see the video I uploaded. In the first tap, the animation is working as expected, but in the second tap the issue occurs. Please advice.
Video showing the problem
- (void)animateTransition:(nonnull id<UIViewControllerContextTransitioning>)transitionContext {
// 1
UIView *containerView = transitionContext.containerView;
UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// 2
[containerView addSubview:toViewController.view];
if([toViewController isKindOfClass:[ImageViewController class]]){
ImageViewController *vcImage = (ImageViewController*)toViewController;
vcImage.constraintLeft.constant = 20;
vcImage.constraintTop.constant = self.selectedCardFrame.origin.y + 60.0;
vcImage.constraintRight.constant = 20.0;
vcImage.constraintBottom.constant = 0;//self.CollectionView.bounds.size.height - (self.selectedCardFrame.origin.y + self.selectedCardFrame.size.height);
[toViewController.view layoutIfNeeded];
NSTimeInterval duration = [self transitionDuration:transitionContext];
[toViewController.view layoutIfNeeded];
[UIView animateWithDuration:duration animations:^{
//First animation block
vcImage.constraintRight.constant = -20.0;
vcImage.constraintLeft.constant = -20.0;
vcImage.constraintTop.constant = -20.0;
vcImage.constraintBottom.constant = 0;
[toViewController.view layoutIfNeeded];
//toViewController.configureRoundedCorners(shouldRound: false)
} completion:^(BOOL finished) {
//Second animation block
[UIView animateWithDuration:duration animations:^{
vcImage.constraintLeft.constant = 0;
vcImage.constraintTop.constant = 0.0;
vcImage.constraintRight.constant = 0;
vcImage.constraintBottom.constant = 0;
[toViewController.view layoutIfNeeded];
//toViewController.configureRoundedCorners(shouldRound: false)
} completion:^(BOOL finished) {
[transitionContext completeTransition:finished];
}];
}];
}
}
Updated code after #Sh_Khan 's comment.
- (void)animateTransition:(nonnull id<UIViewControllerContextTransitioning>)transitionContext {
// 1
UIView *containerView = transitionContext.containerView;
UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// 2
[containerView addSubview:toViewController.view];
if([toViewController isKindOfClass:[ImageViewController class]]){
ImageViewController *vcImage = (ImageViewController*)toViewController;
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:0 animations:^{
vcImage.constraintLeft.constant = 20;
vcImage.constraintTop.constant = self.selectedCardFrame.origin.y + 60.0;
vcImage.constraintRight.constant = 20.0;
vcImage.constraintBottom.constant = 0;
[toViewController.view layoutIfNeeded];
} completion:^(BOOL finished) {
[UIView animateWithDuration:duration animations:^{
//First animation block
vcImage.constraintRight.constant = -20.0;
vcImage.constraintLeft.constant = -20.0;
vcImage.constraintTop.constant = -20.0;
vcImage.constraintBottom.constant = 0;
[toViewController.view layoutIfNeeded];
//toViewController.configureRoundedCorners(shouldRound: false)
} completion:^(BOOL finished) {
//Second animation block
[UIView animateWithDuration:duration animations:^{
vcImage.constraintLeft.constant = 0;
vcImage.constraintTop.constant = 0.0;
vcImage.constraintRight.constant = 0;
vcImage.constraintBottom.constant = 0;
[toViewController.view layoutIfNeeded];
//toViewController.configureRoundedCorners(shouldRound: false)
} completion:^(BOOL finished) {
[transitionContext completeTransition:finished];
}];
}];
}];
}
}
Before setting your imageView try reorienting the photo. Something tells me the image view is having a hard time determining the orientation of the image. I had this happen during a transition once and would explain why you are seeing the behavior only some of the time. I am a bit rusty with Objective C but give this a shot and use it to before setting your imageView image.
-(UIImage*)normalizeImage:(UIImage*)currentImage {
if (currentImage.imageOrientation == UIImageOrientationUp){
return currentImage;
}
UIGraphicsBeginImageContextWithOptions(currentImage.size, NO, currentImage.scale);
[currentImage drawInRect:CGRectMake(0, 0, currentImage.size.width, currentImage.size.height)];
UIImage *_newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return _newImage;
}
I used below function for 3dRotation on view. But i don't want tilt/flip on view, I just want to left/right/up/down movement on view.
How i avoid tilt and flip rotation on my view ?
- (void)Move3dPan:(UIPanGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateChanged)
{
CGPoint displacement = [gesture translationInView:self.view];
CATransform3D currentTransform = self.popUpView.layer.sublayerTransform;
if (displacement.x==0 && displacement.y==0)
{
// no rotation, nothing to do
return;
}
CGFloat totalRotation = sqrt(displacement.x * displacement.x + displacement.y * displacement.y) * M_PI / 360.0;
CGFloat xRotationFactor = displacement.x/totalRotation;
CGFloat yRotationFactor = displacement.y/totalRotation;
CATransform3D rotationalTransform = CATransform3DRotate(currentTransform, totalRotation,
(xRotationFactor * currentTransform.m12 - yRotationFactor * currentTransform.m11),
(xRotationFactor * currentTransform.m22 - yRotationFactor * currentTransform.m21),
(xRotationFactor * currentTransform.m32 - yRotationFactor * currentTransform.m31));
[CATransaction setAnimationDuration:0];
self.popUpView.layer.sublayerTransform = rotationalTransform;
[gesture setTranslation:CGPointZero inView:self.view];
}
}
-(void) startAnimatingCupView
{
[self stopAnimatingCupView];
timer = [NSTimer scheduledTimerWithTimeInterval:0.8 repeats:true block:^(NSTimer * _Nonnull timer) {
[self animateCupView];
}];
}
-(void) animateCupView{
[UIView animateWithDuration:0.4 animations:^{
self.imgCup.transform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(180));
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.4 animations:^{
self.imgCup.transform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(360));
}];
}];
}
-(void) stopAnimatingCupView{
if (timer != nil) {
[timer invalidate];
timer = nil;
}
[self.imgCup.layer setTransform:CATransform3DIdentity];
}
i hope this will work
I'm using this code to animate buttons to bounce with different delays:
- (void) animateWithDuration: (float) duration andObject: (UIButton *) buttonToAnimate{
buttonToAnimate.hidden = YES;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, duration * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self animateButton: buttonToAnimate];
});
}
- (void) animateButtons
{
[self animateWithDuration:0.3 andObject:self.photoAlbum];
[self animateWithDuration:0.7 andObject:self.camera];
[self animateWithDuration: 0.45 andObject:self.helpInfo];
}
- (void) animateButton: (UIButton *) button
{
button.transform = CGAffineTransformScale(CGAffineTransformIdentity, 0.001, 0.001);
button.hidden = NO;
[UIView animateWithDuration:0.3/0.7 animations:^{
button.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1.1, 1.1);
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.3/1.0 animations:^{
button.transform = CGAffineTransformScale(CGAffineTransformIdentity, 0.9, 0.9);
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.3/1.0 animations:^{
button.transform = CGAffineTransformIdentity;
}];
}];
}];
}
It works perfectly on iphone, but on ipad (simulator) the buttons makes a little jump up to the left with about 20 points on the completion block. Then back 20 point on the second completion. Using option: UIViewAnimationOptionTransitionNone (or any of the options) on the animation lessens the jump a tiny bit but you can still see it. Any idea whats wrong?
In my application I have a view that sits on top of another view. The user is able to swipe the top view to the right, which fades it out and "fades in" the back view.
When the user pans to the right and lets go, the animation kicks in and it should translate from its current position to the new off screen position. The problem is that the animation instead snaps the view back to its starting point and then does the animation.
Here is my code for the pan gesture and animation:
- (void)handlePanGesture:(UIPanGestureRecognizer *)recognizer
{
CGPoint translation = [recognizer translationInView:recognizer.view];
CGPoint velocity = [recognizer velocityInView:recognizer.view];
switch (recognizer.state) {
case UIGestureRecognizerStateBegan: {
[recognizer setTranslation:CGPointMake(self.slidingView.frame.origin.x, 0) inView:recognizer.view];
break;
}
case UIGestureRecognizerStateChanged: {
[self.slidingView setTransform:CGAffineTransformMakeTranslation(MAX(0,translation.x), 0)];
CGFloat percentage = fmaxf(0,(translation.x/recognizer.view.bounds.size.width));
[self.backgroundBlurImageView setAlpha:1-percentage];
[self.slidingView setAlpha:1-percentage];
[self.backgroundImageView setTransform:CGAffineTransformMakeScale(1 + (percentage * 0.05), 1 + (percentage * 0.05))];
[self.backgroundBlurImageView setTransform:CGAffineTransformMakeScale(1 + (percentage * 0.05), 1 + (percentage * 0.05))];
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
if (velocity.x > 5.0 || (velocity.x >= -1.0 && translation.x > kMenuViewControllerMinimumPanDistanceToOpen * self.slidingView.bounds.size.width)) {
CGFloat transformedVelocity = velocity.x/ABS(self.slidingView.bounds.size.width - translation.x);
CGFloat duration = 0.66;
[self showViewAnimated:YES duration:duration initialVelocity:transformedVelocity];
} else {
[self hideViewAnimated:YES];
}
}
default:
break;
}
}
- (void)showViewAnimated:(BOOL)animated duration:(CGFloat)duration
initialVelocity:(CGFloat)velocity;
{
// animate
__weak typeof(self) blockSelf = self;
[UIView animateWithDuration:animated ? duration : 0.0 delay:0
usingSpringWithDamping:0.8f initialSpringVelocity:velocity options:UIViewAnimationOptionAllowUserInteraction animations:^{
blockSelf.slidingView.transform = CGAffineTransformMakeTranslation(blockSelf.slidingView.bounds.size.width, 0);
[blockSelf.backgroundBlurImageView setTransform:CGAffineTransformMakeScale(1.05, 1.05)];
[blockSelf.backgroundImageView setTransform:CGAffineTransformMakeScale(1.05, 1.05)];
[blockSelf.backgroundBlurImageView setAlpha:0];
[blockSelf.slidingView setAlpha:0];
} completion:^(BOOL finished) {
blockSelf.tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(didTapOnAaron:)];
[blockSelf.view addGestureRecognizer:self.tapGestureRecognizer];
}];
}
- (void)hideViewAnimated:(BOOL)animated;
{
__weak typeof(self) blockSelf = self;
[UIView animateWithDuration:0.3f animations:^{
blockSelf.slidingView.transform = CGAffineTransformIdentity;
[blockSelf.backgroundBlurImageView setTransform:CGAffineTransformIdentity];
[blockSelf.backgroundImageView setTransform:CGAffineTransformIdentity];
[blockSelf.backgroundBlurImageView setAlpha:1];
[blockSelf.slidingView setAlpha:1];
} completion:^(BOOL finished) {
[blockSelf.view removeGestureRecognizer:self.tapGestureRecognizer];
blockSelf.tapGestureRecognizer = nil;
}];
}
I have already tried settings the animation options to:
UIViewAnimationOptionBeginFromCurrentState
with no effect.
Anyone have a clue what I am missing? Thanks
Kyle, sometimes this sort of thing happens if you are using autolayout. If you don't need it try disabling it and see if it fixes it.
I'm trying to move a UILabel from a starting position of off the top of my UIViewController in a smooth slide type animation so that it slides down from the top when the view loads and then stops at a y position for about 10 seconds. After 10 seconds I want to slide back off the view again.
-(void) animateInstructionLabel
{
float newX = 50.0f;
float newY = 100.0f;
[UIView transitionWithView:self.lblInstruction
duration:10.0f
options:UIViewAnimationCurveEaseInOut
animations:^(void) {
lblInstruction.center = CGPointMake(newX, newY);
}
completion:^(BOOL finished) {
// Do nothing
}];
}
But I don't know how to do the 10 second delay and then move back within the method above. The end result is that I want this to be a label that appears like a notification and then moves off screen again.
Could someone hep me put these pieces described above together?
EDIT
I am adding this based on answer below:
-(void) animateInstructionLabel
{
float newX = lblInstruction.frame.origin.x;
float newY = 20.0f;
lblInstruction.center = CGPointMake(lblInstruction.frame.origin.x, -20.0f);
lblInstruction.bounds = CGRectMake(lblInstruction.frame.origin.x, -20.0f, 650.0f, 40.0f);
[UIView animateWithDuration:3.0f
delay:0
options:UIViewAnimationCurveEaseInOut
animations:^(void) {
lblInstruction.center = CGPointMake(newX, newY);
}
completion:^(BOOL finished) {
[UIView animateWithDuration:5.0f
delay:5.0
options:UIViewAnimationCurveEaseInOut
animations:^(void) {
lblInstruction.center = CGPointMake(newX, -20);
}
completion:^(BOOL finished) {
// Do nothing
}
];
}
];
}
But even though I am setting the label.center off the screen before the animation starts I am seeing that it moves from top left corner of the viewcontroller to the center of the viewcontroller. It is not keeping the x position that it should be set to before the animation begins.
Try adding this after your animation block (oldX,oldY are the old coordinates, before animating the label in):
double delayInSeconds = 10.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[UIView transitionWithView:self.lblInstruction
duration:10.0f
options:UIViewAnimationCurveEaseInOut
animations:^(void) {
lblInstruction.center = CGPointMake(oldX, oldY);
}
completion:^(BOOL finished) {
// Do nothing
}];
});
Something like this should work:
-(void) animateInstructionLabel {
float newX = 50.0f;
float newY = 100.0f;
labelInstruction.center = CGPointMake(newX, -20); // start off screen
[UIView animateWithDuration:10.0f
delay:0
options:UIViewAnimationCurveEaseInOut
animations:^(void) {
lblInstruction.center = CGPointMake(newX, newY);
}
completion:^(BOOL finished) {
[UIView animateWithDuration:10.0f
delay:10.0
options:UIViewAnimationCurveEaseInOut
animations:^(void) {
lblInstruction.center = CGPointMake(newX, -20);
}
completion:^(BOOL finished) {
// Do nothing
}
];
}
];
}
Here is what worked in case it helps somebody else. I had to use .frame instead of .center to set the initial position and then the subsequent animation back to that position.
-(void) animateInstructionLabel
{
lblInstruction.frame = CGRectMake(lblInstruction.frame.origin.x, -40.0f, 650.0f, 40.0f);
[self.view addSubview:lblInstruction];
lblInstruction.hidden = NO;
float newX = lblInstruction.frame.origin.x;
float newY = 20.0f;
[UIView animateWithDuration:3.0f
delay:0
options:UIViewAnimationCurveEaseInOut
animations:^(void) {
lblInstruction.center = CGPointMake(newX, newY);
}
completion:^(BOOL finished) {
[UIView animateWithDuration:3.0f
delay:5.0
options:UIViewAnimationCurveEaseInOut
animations:^(void) {
lblInstruction.frame = CGRectMake(lblInstruction.frame.origin.x, -40.0f, 650.0f, 40.0f);
}
completion:^(BOOL finished) {
lblInstruction.hidden = YES;
}
];
}
];
}
This smoothly makes the label slide down from off the view, stop for 5 seconds and then smoothly moves it back vertically off the view.
Thanks for the previous answers which lead me to the above final solution.