Navigating UIViewControllers with gestures in iOS

后端 未结 1 1035
轮回少年
轮回少年 2020-12-29 16:45

I have several view controllers embedded in a UINavigationController (some modal, some pushed) and am navigating through them using swipe gestures as such:

/         


        
1条回答
  •  伪装坚强ぢ
    2020-12-29 17:06

    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 
    
    - (void)pushChildViewController:(UIViewController *)newChildController;
    - (void)popChildViewController;
    
    @end
    
    @interface ParentViewController : UIViewController 
    ...
    @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 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 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.

    0 讨论(0)
提交回复
热议问题