Navigating with UIViewControllers using gestures in iOS

I have several view controllers built into the UINavigationController (some modal, some pressed), and I navigate through them using gesture wipes 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 great, but I want the user to be able to physically drag the presented modally presented view controller, for example, down and turn off the screen, and not just click, but an animation - everything else or drag directly on the screen and snap to the previous view instead of clicking the button " Back. "

I tried to implement this using the panorama gesture in the view, but, of course, the previous view controller is not visible behind it as it should be. How is this effect achieved properly? View controller containment? If so, how will this work when pushing multiple view controllers onto the stack? An example of the type of navigation I'm talking about can be found in the LetterPress app.

thanks.

+4
source share
1 answer

Yes, the custom container view is the way to go (iOS 5 and above). You basically write your own custom container using the childViewControllers built-in property to track all child controllers. You may need your own property, say currentChildIndex , to keep track of which child controller you are currently using:

 @property (nonatomic) NSInteger currentChildIndex; 

Your parent controller probably has some push and pop methods for non-scroll navigation, for example:

 - (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 my parent controller is configured to conform to this protocol:

 @protocol ParentControllerDelegate <NSObject> - (void)pushChildViewController:(UIViewController *)newChildController; - (void)popChildViewController; @end @interface ParentViewController : UIViewController <ParentControllerDelegate> ... @end 

Then, when the child wants to click on a new child, he can do it as follows:

 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 jump out, he can do it like this:

 id<ParentControllerDelegate> parent = (id)self.parentViewController; NSAssert([parent conformsToProtocol:@protocol(ParentControllerDelegate)], @"Parent must conform to ParentControllerDelegate"); [parent popChildViewController]; 

And then the parent view controller is configured for pan gestures to handle panning the user 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 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 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 are doing up and down panning, not the left-right panning that I used above, but hopefully you get the main idea.


By the way, in iOS 6, the user interface you are asking about (sliding between views using gestures) could perhaps be done more efficiently using the built-in container controller, UIPageViewController . Just use the UIPageViewControllerTransitionStyleScroll transition UIPageViewControllerTransitionStyleScroll and the UIPageViewControllerNavigationOrientationHorizontal navigation UIPageViewControllerNavigationOrientationHorizontal . Unfortunately, iOS 5 only allows page transitions, and Apple only introduces the scroll transitions you want in iOS 6, but if that's all you need, the UIPageViewController does the job even more efficiently than what I set out above (for you no need to make any custom container calls, write gesture recognizer entries, etc.).

For example, you can drag the “page view controller” onto your storyboard, subclass UIPageViewController , 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 be different (at least by different class names and different storyboard identifiers, I also allow the page view controller to instantiate the next page controller when the user requests it, and because I do not keep any strong links for them, they will be released when I go to another page ... you could just create an instance at startup, and then these before and after routines obviously did not create an instance, but rather would look for them in an array), but hopefully you get those are the idea.

But the key problem is that I have no gesture code, no custom container controller code, etc. Much easier.

+9
source

All Articles