this is what I like to achieve:
I want to perform a slide up animation where a user can slide a UIView2 up, and the UIView2 will stop half-way on the screen.
I know how to present a UIViewControler modally via a UIButton action like so:
[self presentViewController:newController animated:NO completion:nil];
I also know you need this type of code for animations:
[UIView animateWithDuration:0.5f animations:^{
[self.customView layoutIfNeeded];
} completion:^(BOOL finished) {
}];
However, I'm not sure how to achieve this with UIViews, especially since I use AutoLayout for my project and I use constraints to easily support multiple screen sizes.
How can I achieve this without breaking AutoLayout and the constraints I have set up?
Thanks
What I typically do in this situation is the following.
Add two constraints to your scene. One where UIView2
is aligned to the bottom of UIView1
. The second, where it's aligned center of UIView1
(you'll want to ctrl+drag between the views to add the constraints appropriately). These constraints will initially conflict with one another, and that's fine.
Add IBOutlet
s to your view controller for the NSLayoutConstraints
and assign the two constraints we've created to those IBOutlet
s.
Set the constraint priority on your initial condition to 999 (i.e. constraint priority on align to bottom should be 999). Set the constraint priority on the destination constraint to 998 (i.e. constraint priority on align center is 998). You'll now see that these constraints will no longer conflict. This is because the priority on one constraint overrides the other.
You may see where this is headed now. So when you want to animate UIView2
between the constraints, swap the priorities and animate!
Code:
@interface MyViewController ()
@property (nonatomic, weak) IBOutlet NSLayoutConstraint* constraint0;
@property (nonatomic, weak) IBOutlet NSLayoutConstraint* constraint1;
@end
- (void)someMethodWhereIWantToAnimate
{
NSInteger temp = self.constraint0.priority;
self.constraint0.priority = self.constraint1.priority;
self.constraint1.priority = temp;
[UIView animateWithDuration:0.3 animations:^{
// Simplest is to use the main ViewController's view
[self.view layoutIfNeeded];
}];
}
To enable panning and using that to initiate your animation add a Pan Gesture Recognizer to your view controller. Ctrl+drag from UIView2
to the Pan Gesture Recognizer and set it as a gestureRecognizer
. Now when you drag on UIView2
, you'll be able to receive pan events.
Add an IBAction to handle the pan:
- (IBAction)onPan:(id)sender
{
}
Ctrl+drag from the pan gesture recognizer to the view controller and set onPan:
as the sent action.
sender
is actually the pan gesture recognizer itself. So we'll be able to fill out this method to handle a pan and have UIView2
follow under the user's finger and then initiate the animation when they let go.
Let's start filling out the code:
- (IBAction)onPan:(id)sender
{
UIPanGestureRecognizer* recognizer = (UIPanGestureRecognizer*)sender;
CGPoint translation = [recognizer translationInView:self.view];
NSLog(@"State: (%d) Translation in view: (%f, %f)", recognizer.state, translation.x, translation.y);
}
If you run with this code, and drag your finger over UIView2
, you'll see output like this:
State: (1) Translation in view: (0.000000, -2.500000)
State: (2) Translation in view: (0.500000, -7.500000)
State: (2) Translation in view: (0.500000, -7.500000)
State: (2) Translation in view: (1.500000, -12.000000)
State: (2) Translation in view: (2.500000, -16.500000)
State: (2) Translation in view: (2.500000, -19.500000)
State: (2) Translation in view: (2.500000, -24.500000)
State: (2) Translation in view: (2.500000, -25.000000)
State: (2) Translation in view: (2.500000, -25.500000)
State: (2) Translation in view: (2.500000, -27.000000)
State: (2) Translation in view: (2.500000, -29.500000)
State: (2) Translation in view: (2.500000, -31.000000)
State: (2) Translation in view: (2.500000, -31.500000)
State: (3) Translation in view: (2.500000, -31.500000)
Notice that the values are constantly increasing. We'll want the incremental amount. Notice the state in the log. State (1) is drag start. State (2) is drag changed. State (3) is drag ended. Using this information, we can calculate the delta.
Add a CGPoint
property to your view controller and add UIView2 as an IBOutlet as well:
@property (nonatomic) CGPoint lastTranslation;
@property (nonatomic, weak) IBOutlet UIView* uiView2;
Finally, let's fill out the final form of our pan method:
- (IBAction)onPan:(id)sender
{
UIPanGestureRecognizer* recognizer = (UIPanGestureRecognizer*)sender;
CGPoint delta;
switch(recognizer.state) {
case UIGestureRecognizerStateBegan:
// On state begin: delta is the translation. Store it in our member for later user.
self.lastTranslation = delta = [recognizer translationInView:self.view];
break;
case UIGestureRecognizerStateChanged:
// On state changed: calculate the difference between the translation and our
// previous translation.
delta = CGPointApplyAffineTransform([recognizer translationInView:self.view], CGAffineTransformMakeTranslation(-self.lastTranslation.x, -self.lastTranslation.y));
self.lastTranslation = [recognizer translationInView:self.view];
break;
case UIGestureRecognizerStateEnded:
// On state ended: Let's just do the constraint animation.
[self someMethodWhereIWantToAnimate];
break;
default:
break;
}
delta.x = 0; // Forces only vertical drag on the UIView2.
self.uiView2.center = CGPointApplyAffineTransform(self.uiView2.center, CGAffineTransformMakeTranslation(delta.x, delta.y)); // Move our uiView2 based on the delta.
NSLog(@"State: (%d) Translation in view: (%f, %f)", recognizer.state, delta.x, delta.y);
}
This should get you well on your way. You may want to look at tweaking the UIView animation on the constraints a bit based on the pan gesture's
velocityInView:
method, but that I'll leave as an exercise.