Dear Core Animation/iOS Experts,
I am trying to get an effect similar to the question below.
Core animation animating the layer to flip horizontally
The accepted answer seems to describe exactly what I require, except there is no code with this answer and I can't get any code I write to work.
Here is exactly what I'm trying to do:
1) Have 1 master view controller/view and on a portion of the main view have 2 UIViews which overlap, with only one shown (1 at the 'front' and 1 at the 'back')
2) A separate UIControl/UIButton gets pressed and then a 3D flip transition occurs which rotates the front (visible) view out of view and at same time rotates the back (hidden) view to the front...just like seeing the reverse of a playing card.
3) Be able to keep pressing the UIControl to toggle between the two views
4) Be able to interact with just the controls on the front view (i.e. pressing the front layer won't inadvertently fire a control on the back which happens to lie underneath the tap)
I may be approaching this the wrong way so let me know. Ideally, I would like to use Core Animation, and not the UIView built in flip transactions, as I want the animation to be 3D and also want to use this task as a stepping stone for doing more complex CA stuff.
At the moment I can get the front view to flip nicely (only once) but the back view doesn't show.
Cheers,
Andy
Here is the code I've got:
MainViewController.h
#interface MainViewController : UIViewController
- (IBAction)changeViewTapped:(UITapGestureRecognizer *)recognizer;
#end
MainViewController.m
#import "MainViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface MainViewController ()
#property (weak, nonatomic) IBOutlet UIView *detailView;
#property (weak, nonatomic) IBOutlet UIView *listView;
#property (nonatomic) CATransform3D rotationAndPerspectiveTransform;
#end
#implementation MainViewController
#synthesize detailView = _detailView;
#synthesize listView = _listView;
#synthesize rotationAndPerspectiveTransform = _rotationAndPerspectiveTransform;
- (void)viewDidLoad {
[super viewDidLoad];
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;
rotationAndPerspectiveTransform.m34 = 1.0 / -500;
rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, M_PI, 0.0f, 1.0f, 0.0f);
self.rotationAndPerspectiveTransform = rotationAndPerspectiveTransform;
CALayer *listLayer = self.listView.layer;
listLayer.doubleSided = NO;
listLayer.transform = self.rotationAndPerspectiveTransform;
}
- (IBAction)changeViewTapped:(UITapGestureRecognizer *)recognizer {
CALayer *detailLayer = self.detailView.layer;
CALayer *listLayer = self.listView.layer;
detailLayer.doubleSided = NO;
listLayer.doubleSided = NO;
[UIView animateWithDuration:0.5 animations:^{
detailLayer.transform = self.rotationAndPerspectiveTransform;
listLayer.transform = self.rotationAndPerspectiveTransform;
} completion:^(BOOL finished){
// code to be executed when flip is completed
}];
}
#end
A quick tip about the flip animation in iOS -- the system needs time to render the back view (the one that will be flipped to) before you start the flip animation. So if you try to change the contents of that view, and trigger off the flip animation in the same method, you will have problems.
To get around this, I've had success with code like this:
- (void)flipView {
// Setup the view for the back side of the flip
[self performSelector:#selector(performFlip) withObject:nil afterDelay: 0.1];
}
- (void)performFlip {
[UIView transitionWithView: tileToFlip
duration: 0.5
options: UIViewAnimationOptionTransitionFlipFromLeft
animations:^{
// My flip specific code
}
completion:^(BOOL finished) {
}
];
}
In a nutshell, I'm setting everything up in the flipView method, returning control to iOS so it has time to do it's rendering, then kicking off the flipTile selector after a tenth of a second to do the actual animation.
Good luck!
- (void)perform
{
UIViewController *src = (UIViewController *) self.sourceViewController;
UIViewController *dst = (UIViewController *) self.destinationViewController;
[UIView beginAnimations:#"LeftFlip" context:nil];
[UIView setAnimationDuration:0.8];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:src.view.superview cache:YES];
[UIView commitAnimations];
[src presentViewController:dst animated:NO completion:nil];
}
Related
#interface ViewController
#property(weak) IBOutlet UIView *blackView;
#end
(1) First I tried to use animation block
- (IBAction) buttonHitted:(id)sender
{
[UIView beginAnimations: nil context: NULL];
[UIView setAnimationBeginsFromCurrentStatus: NO];
self.blackView.center = CGPointMake(UIScreen.mainScreen.bounds.size.width - self.blackView.center.x, self.blackView.center.y);
[UIView commitAnimations];
}
And the Animations ALWAYS begins from current status, and I can't find out why for long.
(2)Then I tried to use code block, it is the sad same.
- (IBAction) buttonHitted:(id)sender
{
[UIView animateWithDuration:2.0 delay:0.0 options:0
animations:^(void)
{
self.blackView.center = CGPointMake(UIScreen.mainScreen.bounds.size.width - self.blackView.center.x, self.blackView.center.y);
}
completion:nil];
}
//Options to 0, indicating that UIViewAnimationOptionBeginFromCurrentState not setted
I just wanna know why the animation ALWAYS begins from current status. I checked the property of the view, it is always the last value of the current animation.
I think I found out the reason behind it.
Animations for property(like UIView_ins.center and CALayer_ins.transform) could be mixed together, even you just changed one part of it, like just center.x
What's more, CAAnimation must set this property to YES, so the default CAAction for the transform must set this to YES too.
#property(getter=isAdditive) BOOL additive;
SO the new animation do not override the flight-in one, but just mixed them together.
To make sure it change UIView animation options to UIViewAnimationOptionCurveLinear
- (IBAction) buttonHitted:(id)sender
{
[UIView animateWithDuration:3.0 delay:0.0
options:UIViewAnimationOptionCurveLinear
animations:^(void)
{
self.blackView.center = CGPointMake(UIScreen.mainScreen.bounds.size.width - self.blackView.center.x, self.blackView.center.y);
}
completion:nil];
}
You will see the lovely UIView standing there for a long time and did nothing, that is the symbol of mixed in linear way :)
While using the default EaseInOut options, it looks like the problem of setAnimationBeginsFromCurrentStatus:.
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.
Short description: When the keyboard is shown I translate a button up, but upon clicking the button it pops back down to its original position.
Long description: I have two buttons, one on top of the other. One is hidden until certain criteria are met, then it becomes visible and the other is hidden.
#property (weak, nonatomic) IBOutlet UIButton *skipButton;
#property (weak, nonatomic) IBOutlet UIButton *saveButton;
#property (assign, nonatomic) BOOL saveButtonEnabled;
#property (assign, readonly, nonatomic) CGRect fieldContainerDefaultFrame;
#property (assign, readonly, nonatomic) CGRect skipButtonDefaultFrame;
When view loads I cache the default positions
- (void)viewDidLoad
{
[super viewDidLoad];
...
_fieldContainerDefaultFrame = self.fieldContainerView.frame;
_skipButtonDefaultFrame = self.skipButton.frame;
[self.saveButton setHidden:YES];
self.saveButtonEnabled = NO;
}
The buttons are wired to outlets correctly and these are their "touch up inside" actions
#pragma mark - Actions
- (IBAction)didPressSaveButton:(id)sender
{
// Code here to persist data
// Code here to push segue to next scene
}
- (IBAction)didPressSkipButton:(id)sender
{
// Code here to push segue to next scene
}
Here are the methods that respond to the keyboard notifications. I do register for the notifications I'm just not including that code. (There is no issue there, all this stuff gets run correctly.)
NOTE: Both buttons are contained in the same "container" frame (along with other fields), which translates up. The buttons translate up an additional amount. The translation works in general, the only issue is the reseting behavior of the button upon click.
#pragma mark - Notification handlers
- (void)handleKeyboardWillShowNotification:(NSNotification *)note
{
if([self.bioTextView isFirstResponder])
{
CGRect newFieldContainerFrame = self.fieldContainerDefaultFrame;
CGRect newSkipButtonFrame = self.skipButtonDefaultFrame;
newFieldContainerFrame.origin.y += AGSTSignupDetailsFieldContainerEditingOffset;
newSkipButtonFrame.origin.y += AGSTSignupDetailsSkipButtonEditingOffset;
NSTimeInterval duration = [note.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
UIViewAnimationCurve animationCurve = [note.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
[UIView animateWithDuration:duration animations:^{
[UIView setAnimationCurve:animationCurve];
self.fieldContainerView.frame = newFieldContainerFrame;
self.skipButton.frame = newSkipButtonFrame;
self.saveButton.frame = newSkipButtonFrame;
}];
}
}
- (void)handleKeyboardWillHideNotification:(NSNotification *)note
{
NSTimeInterval duration = [note.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
UIViewAnimationCurve animationCurve = [note.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
[UIView animateWithDuration:duration animations:^{
[UIView setAnimationCurve:animationCurve];
self.fieldContainerView.frame = self.fieldContainerDefaultFrame;
self.skipButton.frame = self.skipButtonDefaultFrame;
self.saveButton.frame = self.skipButtonDefaultFrame;
}];
}
I'd appreciate any insights or suggestions as to how to fix the bug or how to craft a better solution. Thanks.
Okay, I fixed the problem. While I appreciate rob mayoff's response as an indication of better practices (i.e., don't set frames when using autolayout), that was not the actual cause of the problem in my case, thus I don't consider my Q&A to be a duplicate of anything I could find. I'm going to post this answer in case someone else runs across a similar problem.
It turns out, as I suspected earlier this morning, that the button's push segue did two mutually incompatible things: (1) it caused the keyboard to disappear, which caused the fields to be translated downward; and (2) it caused the whole scene to animate away as the new VC comes into place, which apparently cancelled all pending animations (i.e., the translation) and caused those translations to abruptly jump to their destination states.
The quickest solution was to set a BOOL didPressSaveButton to NO on viewDidLoad and only set it to YES once the button is pushed, and then check that BOOL before every relevant translation animation: if the BOOL is NO, go ahead and do the animation, otherwise do nothing. This will prevent the abrupt final animation before the push segue.
The better solution, and one I will implement soon, will be to replace my use of frames with autolayout constraints.
Custom transitions work easily with standard containers and while presenting modal view controllers. But what about using custom transitions with a totally custom container?
I'd like to use the UIViewControllerContextTransitioning protocol with my custom container and take advantage of transitioning and interactive transitions.
In the comment into the header file of UIViewControllerContextTransitioning I read:
// The UIViewControllerContextTransitioning protocol can be adopted by custom
// container controllers. It is purposely general to cover more complex
// transitions than the system currently supports.
but I can't understand how to create a Context Transitioning and start all the custom transition process.
This is very well possible!
have a look at this SO Answer.
You just have to create a viewController conforming to the UIViewControllerContextTransitioning protocol and use the ViewController Containment API (addChildViewController:, willMoveToParentViewController:, and so on).
When you want to start the custom transition, simply call the [animateTransition] Method on your transitionController. From that point on, it's the same as with the Apple-provided API.
Make your custom container fit in with the Transitioning API.
In fact, all the new protocols introduced in iOS7 (UIViewControllerContextTransitioning and so on) are not really needed to implement your own fully custom containerViewController. Its just providing a neat class-relationship and method pool. But you could write that all by yourself and would not need to use any private API.
ViewController Containment (introduced in iOS5) is all you need for this job.
I just tried gave it a try and its even working with interactive transitions.
If you need some sample code please let me know and i'll provide some :)
EDIT
By the way, this is how Facebook and Instagram for iOS is creating their navigation concept. You can scroll away the NavigationBar in these Apps and when a new ViewController is "pushed" the NavigationBar seems to have been reset during the pushing.
Ironically, i asked the exact same question a month ago here on StackOverflow :D
My Question
And I just realized i answered it myself :)
Have a nice day !
In fact, it says so but you are incomplete:
The UIViewControllerContextTransitioning protocol can be adopted by
custom container controllers. It is purposely general to cover
more complex transitions than the system currently supports. For
now, navigation push/pops and UIViewController present/dismiss
transitions can be customized.
That means you can trigger your VC's transitioningDelegate by calling presentViewController:animated:completion: method or you can implement navigationController:animationControllerForOperation:fromViewController:toViewController: in your UINavigationControllerDelegate class and return an object that conforms UIViewControllerAnimatedTransitioning protocol.
To sum up, there are only two ways to provide custom transition. Both requires presenting/pushing a VC while we add subviews to create custom container view controllers.
Probably, Apple will do what they said in the first lines of their statement in header file in the future, but for now what you want to do seems impossible.
From docs UIViewControllerContextTransitioning
Do not adopt this protocol in your own classes, nor should you
directly create objects that adopt this protocol.
Please look at MPFoldTransition, it worked for me.
You can also use the following Category, you can create your own transition using this.
#import <UIKit/UIKit.h>
#interface UIView (Animation)
- (void) moveTo:(CGPoint)destination duration:(float)secs option:(UIViewAnimationOptions)option;
- (void) downUnder:(float)secs option:(UIViewAnimationOptions)option;
- (void) addSubviewWithZoomInAnimation:(UIView*)view duration:(float)secs option:(UIViewAnimationOptions)option;
- (void) removeWithZoomOutAnimation:(float)secs option:(UIViewAnimationOptions)option;
- (void) addSubviewWithFadeAnimation:(UIView*)view duration:(float)secs option:(UIViewAnimationOptions)option;
- (void) removeWithSinkAnimation:(int)steps;
- (void) removeWithSinkAnimationRotateTimer:(NSTimer*) timer;
#end
Implementation :
#import "UIView+Animation.h"
#implementation UIView (Animation)
- (void) moveTo:(CGPoint)destination duration:(float)secs option:(UIViewAnimationOptions)option
{
[UIView animateWithDuration:secs delay:0.0 options:option
animations:^{
self.frame = CGRectMake(destination.x,destination.y, self.frame.size.width, self.frame.size.height);
}
completion:nil];
}
- (void) downUnder:(float)secs option:(UIViewAnimationOptions)option
{
[UIView animateWithDuration:secs delay:0.0 options:option
animations:^{
self.transform = CGAffineTransformRotate(self.transform, M_PI);
self.alpha = self.alpha - 0.5;
}
completion:nil];
}
- (void) addSubviewWithZoomInAnimation:(UIView*)view duration:(float)secs option:(UIViewAnimationOptions)option
{
// first reduce the view to 1/100th of its original dimension
CGAffineTransform trans = CGAffineTransformScale(view.transform, 0.01, 0.01);
view.transform = trans; // do it instantly, no animation
[self addSubview:view];
// now return the view to normal dimension, animating this tranformation
[UIView animateWithDuration:secs delay:0.0 options:option
animations:^{
view.transform = CGAffineTransformScale(view.transform, 100.0, 100.0);
}
completion:nil];
}
- (void) removeWithZoomOutAnimation:(float)secs option:(UIViewAnimationOptions)option
{
[UIView animateWithDuration:secs delay:0.0 options:option
animations:^{
self.transform = CGAffineTransformScale(self.transform, 0.1, 0.1);
}
completion:^(BOOL finished) {
[self removeFromSuperview];
}];
}
// add with a fade-in effect
- (void) addSubviewWithFadeAnimation:(UIView*)view duration:(float)secs option:(UIViewAnimationOptions)option
{
view.alpha = 0.0; // make the view transparent
[self addSubview:view]; // add it
[UIView animateWithDuration:secs delay:0.0 options:option
animations:^{view.alpha = 1.0;}
completion:nil]; // animate the return to visible
}
// remove self making it "drain" from the sink!
- (void) removeWithSinkAnimation:(int)steps
{
//NSTimer *timer;
if (steps > 0 && steps < 100) // just to avoid too much steps
self.tag = steps;
else
self.tag = 50;
[NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:#selector(removeWithSinkAnimationRotateTimer:) userInfo:nil repeats:YES];
}
- (void) removeWithSinkAnimationRotateTimer:(NSTimer*) timer
{
CGAffineTransform trans = CGAffineTransformRotate(CGAffineTransformScale(self.transform, 0.9, 0.9),0.314);
self.transform = trans;
self.alpha = self.alpha * 0.98;
self.tag = self.tag - 1;
if (self.tag <= 0)
{
[timer invalidate];
[self removeFromSuperview];
}
}
#end
I hope it solves your problem. enjoy.:)
I have several view controllers embedded in a UINavigationController (some modal, some pushed) and am navigating through them using swipe gestures as such:
// Gesture recognizers
UISwipeGestureRecognizer *downGesture = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(dismissButton)];
downGesture.direction = UISwipeGestureRecognizerDirectionDown;
[downGesture setCancelsTouchesInView:NO];
[self.view addGestureRecognizer:downGesture];
This works fine, however I want the user to be able to physically drag the modally presented view controller, for example, down and off the screen instead of just a flick and an animation doing the rest, or dragging right across the screen and snapping to the previous view instead of tapping the back button.
I've tried implementing this using a pan gesture on the view but of course the previous view controller isn't visible behind it, which it needs to be. How is this effect achieved properly? With view controller containment? If so how would that work when pushing a few view controllers on to the stack? An example of the type of navigation I'm talking about can be found in the LetterPress app.
thanks.
Yes, custom container view is the way to go (iOS 5 and greater). You basically write your own custom container, using the built-in childViewControllers property to keep track of all of the child view controllers. You may want your own property, say currentChildIndex, to keep track of which child controller you're currently on:
#property (nonatomic) NSInteger currentChildIndex;
Your parent controller should probably have some push and pop methods for non-swipe related navigation, such as:
- (void)pushChildViewController:(UIViewController *)newChildController
{
// remove any other children that we've popped off, but are still lingering about
for (NSInteger index = [self.childViewControllers count] - 1; index > self.currentChildIndex; index--)
{
UIViewController *childController = self.childViewControllers[index];
[childController willMoveToParentViewController:nil];
[childController.view removeFromSuperview];
[childController removeFromParentViewController];
}
// get reference to the current child controller
UIViewController *currentChildController = self.childViewControllers[self.currentChildIndex];
// set new child to be off to the right
CGRect frame = self.containerView.bounds;
frame.origin.x += frame.size.width;
newChildController.view.frame = frame;
// add the new child
[self addChildViewController:newChildController];
[self.containerView addSubview:newChildController.view];
[newChildController didMoveToParentViewController:self];
[UIView animateWithDuration:0.5
animations:^{
CGRect frame = self.containerView.bounds;
newChildController.view.frame = frame;
frame.origin.x -= frame.size.width;
currentChildController.view.frame = frame;
}];
self.currentChildIndex++;
}
- (void)popChildViewController
{
if (self.currentChildIndex == 0)
return;
UIViewController *currentChildController = self.childViewControllers[self.currentChildIndex];
self.currentChildIndex--;
UIViewController *previousChildController = self.childViewControllers[self.currentChildIndex];
CGRect onScreenFrame = self.containerView.bounds;
CGRect offToTheRightFrame = self.containerView.bounds;
offToTheRightFrame.origin.x += offToTheRightFrame.size.width;
[UIView animateWithDuration:0.5
animations:^{
currentChildController.view.frame = offToTheRightFrame;
previousChildController.view.frame = onScreenFrame;
}];
}
Personally, I have a protocol defined for these two methods, and make sure that my parent controller is configured to conform to that protocol:
#protocol ParentControllerDelegate <NSObject>
- (void)pushChildViewController:(UIViewController *)newChildController;
- (void)popChildViewController;
#end
#interface ParentViewController : UIViewController <ParentControllerDelegate>
...
#end
Then, when a child wants to push a new child on, it can do it like so:
ChildViewController *controller = ... // instantiate and configure your next controller however you want to do that
id<ParentControllerDelegate> parent = (id)self.parentViewController;
NSAssert([parent conformsToProtocol:#protocol(ParentControllerDelegate)], #"Parent must conform to ParentControllerDelegate");
[parent pushChildViewController:controller];
When a child wants to pop itself off, it can do it like so:
id<ParentControllerDelegate> parent = (id)self.parentViewController;
NSAssert([parent conformsToProtocol:#protocol(ParentControllerDelegate)], #"Parent must conform to ParentControllerDelegate");
[parent popChildViewController];
And then the parent view controller has a pan gesture set up, to handle the user panning from one child to another:
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
static UIView *currentView;
static UIView *previousView;
static UIView *nextView;
if (gesture.state == UIGestureRecognizerStateBegan)
{
// identify previous view (if any)
if (self.currentChildIndex > 0)
{
UIViewController *previous = self.childViewControllers[self.currentChildIndex - 1];
previousView = previous.view;
}
else
{
previousView = nil;
}
// identify next view (if any)
if (self.currentChildIndex < ([self.childViewControllers count] - 1))
{
UIViewController *next = self.childViewControllers[self.currentChildIndex + 1];
nextView = next.view;
}
else
{
nextView = nil;
}
// identify current view
UIViewController *current = self.childViewControllers[self.currentChildIndex];
currentView = current.view;
}
// if we're in the middle of a pan, let's adjust the center of the views accordingly
CGPoint translation = [gesture translationInView:gesture.view.superview];
previousView.transform = CGAffineTransformMakeTranslation(translation.x, 0.0);
currentView.transform = CGAffineTransformMakeTranslation(translation.x, 0.0);
nextView.transform = CGAffineTransformMakeTranslation(translation.x, 0.0);
// if we're all done, let's animate the completion (or if we didn't move far enough,
// the reversal) of the pan gesture
if (gesture.state == UIGestureRecognizerStateEnded ||
gesture.state == UIGestureRecognizerStateCancelled ||
gesture.state == UIGestureRecognizerStateFailed)
{
CGPoint center = currentView.center;
CGPoint currentCenter = CGPointMake(center.x + translation.x, center.y);
CGPoint offRight = CGPointMake(center.x + currentView.frame.size.width, center.y);
CGPoint offLeft = CGPointMake(center.x - currentView.frame.size.width, center.y);
CGPoint velocity = [gesture velocityInView:gesture.view.superview];
if ((translation.x + velocity.x * 0.5) < (-self.containerView.frame.size.width / 2.0) && nextView)
{
// if we finished pan to left, reset transforms
previousView.transform = CGAffineTransformIdentity;
currentView.transform = CGAffineTransformIdentity;
nextView.transform = CGAffineTransformIdentity;
// set the starting point of the animation to pick up from where
// we had previously transformed the views
CGPoint nextCenter = CGPointMake(nextView.center.x + translation.x, nextView.center.y);
currentView.center = currentCenter;
nextView.center = nextCenter;
// and animate the moving of the views to their final resting points,
// adjusting the currentChildIndex appropriately
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
currentView.center = offLeft;
nextView.center = center;
self.currentChildIndex++;
}
completion:NULL];
}
else if ((translation.x + velocity.x * 0.5) > (self.containerView.frame.size.width / 2.0) && previousView)
{
// if we finished pan to right, reset transforms
previousView.transform = CGAffineTransformIdentity;
currentView.transform = CGAffineTransformIdentity;
nextView.transform = CGAffineTransformIdentity;
// set the starting point of the animation to pick up from where
// we had previously transformed the views
CGPoint previousCenter = CGPointMake(previousView.center.x + translation.x, previousView.center.y);
currentView.center = currentCenter;
previousView.center = previousCenter;
// and animate the moving of the views to their final resting points,
// adjusting the currentChildIndex appropriately
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
currentView.center = offRight;
previousView.center = center;
self.currentChildIndex--;
}
completion:NULL];
}
else
{
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
previousView.transform = CGAffineTransformIdentity;
currentView.transform = CGAffineTransformIdentity;
nextView.transform = CGAffineTransformIdentity;
}
completion:NULL];
}
}
}
It looks like you're doing up and down panning, rather than the left-right panning that I used above, but hopefully you get the basic idea.
By the way, in iOS 6, the user interface you're asking about (the sliding between views using gestures), could probably be done more efficiently using a built-in container controller, UIPageViewController. Just use a transition style of UIPageViewControllerTransitionStyleScroll and a navigation orientation of UIPageViewControllerNavigationOrientationHorizontal. Unfortunately, iOS 5 only allows page curl transitions, and Apple only introduced the scrolling transitions that you want in iOS 6, but if that's all you need, UIPageViewController gets the job done even more efficiently than what I've laid out above (you don't have to do any custom container calls, no writing of gesture recognizers, etc).
For example, you can drag a "page view controller" onto your storyboard, create a UIPageViewController subclass and then in viewDidLoad, you need to configure the first page:
UIViewController *firstPage = [self.storyboard instantiateViewControllerWithIdentifier:#"1"]; // use whatever storyboard id your left page uses
self.viewControllerStack = [NSMutableArray arrayWithObject:firstPage];
[self setViewControllers:#[firstPage]
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:NULL];
self.dataSource = self;
Then you need to define the following UIPageViewControllerDataSource methods:
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
if ([viewController isKindOfClass:[LeftViewController class]])
return [self.storyboard instantiateViewControllerWithIdentifier:#"2"];
return nil;
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
if ([viewController isKindOfClass:[RightViewController class]])
return [self.storyboard instantiateViewControllerWithIdentifier:#"1"];
return nil;
}
Your implementation will vary (at the very least different class names and different storyboard identifiers; I'm also letting the page view controller instantiate the next page's controller when the user asks for it and because I'm not retaining any strong reference to them, the'll be released when I'm done transitioning to the other page ... you could alternatively just instantiate both at startup and then these before and after routines would obviously not instantiate, but rather look them up in an array), but hopefully you get the idea.
But the key issue is that I don't have any gesture code, no custom container view controller code, etc. Much simpler.