【问题标题】:Navigating UIViewControllers with gestures in iOS在 iOS 中使用手势导航 UIViewController
【发布时间】:2013-03-29 12:51:10
【问题描述】:

我在 UINavigationController 中嵌入了几个视图控制器(一些是模态的,一些是推送的),并且正在使用滑动手势在它们中导航:

// Gesture recognizers
UISwipeGestureRecognizer *downGesture = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(dismissButton)];
downGesture.direction = UISwipeGestureRecognizerDirectionDown;
[downGesture setCancelsTouchesInView:NO];
[self.view addGestureRecognizer:downGesture];

这很好用,但是我希望用户能够以物理方式拖动模态呈现的视图控制器,例如,向下和离开屏幕,而不仅仅是轻弹和动画来完成其余的工作,或在屏幕上向右拖动并捕捉到上一个视图,而不是点击后退按钮。

我已经尝试在视图上使用平移手势来实现这一点,但当然前面的视图控制器在它后面是不可见的,这是必须的。这个效果是如何正确实现的?使用视图控制器包含?如果是这样,当将一些视图控制器推入堆栈时,它会如何工作?我正在谈论的导航类型的示例可以在 LetterPress 应用程序中找到。

谢谢。

【问题讨论】:

    标签: ios uiviewcontroller uinavigationcontroller uigesturerecognizer


    【解决方案1】:

    是的,自定义容器视图是可行的方法(iOS 5 及更高版本)。您基本上编写自己的自定义容器,使用内置的childViewControllers 属性来跟踪所有子视图控制器。您可能希望自己的属性(例如 currentChildIndex)来跟踪您当前使用的子控制器:

    @property (nonatomic) NSInteger currentChildIndex;
    

    你的父控制器可能应该有一些非滑动相关导航的推送和弹出方法,例如:

    - (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;
                         }];
    }
    

    就我个人而言,我为这两种方法定义了一个协议,并确保我的父控制器配置为符合该协议:

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

    然后,当一个孩子想推一个新的孩子时,它可以这样做:

    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];
    

    当一个孩子想跳出来时,它可以这样做:

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

    然后父视图控制器设置了平移手势,以处理用户从一个孩子平移到另一个孩子:

    - (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];
            }
        }
    }
    

    看起来您是在进行上下平移,而不是我上面使用的左右平移,但希望您了解基本概念。


    顺便说一句,在 iOS 6 中,您所询问的用户界面(使用手势在视图之间滑动)可能可以使用内置容器控制器 UIPageViewController 更有效地完成。只需使用UIPageViewControllerTransitionStyleScroll 的过渡样式和UIPageViewControllerNavigationOrientationHorizontal 的导航方向。不幸的是,iOS 5 只允许页面卷曲过渡,Apple 只在 iOS 6 中引入了你想要的滚动过渡,但如果这就是你所需要的,UIPageViewController 比我上面列出的更有效地完成工作(您无需进行任何自定义容器调用,无需编写手势识别器等)。

    例如,您可以将“页面视图控制器”拖到故事板上,创建UIPageViewController 子类,然后在viewDidLoad 中,您需要配置第一页:

    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;
    

    那么你需要定义以下UIPageViewControllerDataSource方法:

    - (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;
    }
    

    您的实现会有所不同(至少不同的类名和不同的故事板标识符;我还让页面视图控制器在用户请求时实例化下一页的控制器,因为我没有保留任何强引用对他们来说,当我完成转换到另一个页面时,它们将被释放......您也可以在启动时实例化它们,然后这些 beforeafter 例程显然不会实例化,而是查找它们在一个数组中),但希望你明白这一点。

    但关键问题是我没有任何手势代码,没有自定义容器视图控制器代码等。要简单得多。

    【讨论】:

    • 我花了一段时间才有机会浏览这个,但它看起来是一个很好的起点。有道理,谢谢!
    • @f.perdition 顺便说一下,我提出的方法实现了您在 iOS 5 及更高版本中询问的 UX。如果针对iOS 6及以上,可以使用UIPageViewController,提供视图间的滚动过渡,不用写任何手势代码,简单很多。如果您有兴趣查看该实现,请告诉我。
    • 如果可以的话,那将非常方便。我实际上正在开发仅支持 iOS 6 及更高版本的东西,我应该提到它,再次感谢。
    • 关于如何处理具有UIScrollView 的子控制器的任何建议?
    • @hlfcoding 我在自定义手势中做了三件事。首先,我做了一个仅从屏幕边缘检测到的自定义手势。其次,如果手势没有从适合该边缘的方向开始,我的手势就会失败。第三,我遍历滚动视图的手势并为导航手势调用requireGestureRecognizerToFail,以便它们优先于滚动视图自己的手势。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-09-13
    • 2020-07-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多