关于ios:UIPageViewController手势正在调用viewControllerAfter:但不设置动画

UIPageViewController gesture is calling viewControllerAfter: but doesn't animate

我的UIPageViewController有一个非常有趣的问题。

我的项目的设置与示例基于页面的应用程序模板非常相似。
时不时地(但在一定程度上可再现)某个平移手势会呼出-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController

我返回下一页的viewcontroller,但从未运行过页面翻转动画,也从未调用过我的委托方法。

这是viewControllerAfterViewController的代码

1
2
3
4
5
6
7
-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
    PageDisplayViewController *vc = (PageDisplayViewController *)viewController;
    NSUInteger index = [self.pageFetchController.fetchedObjects indexOfObject:vc.page];
    if(index == (self.pageFetchController.fetchedObjects.count - 1)) return nil;
    return [self getViewControllerForIndex:(++index)];
}

这是getViewControllerForIndex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
-(PageDisplayViewController *)getViewControllerForIndex:(NSUInteger)index
{
    PageDisplayViewController *newVC = [self.storyboard instantiateViewControllerWithIdentifier:@"PageDisplayController"];
    newVC.page = [self.pageFetchController.fetchedObjects objectAtIndex:(index)];
    newVC.view.frame = CGRectMake(0, 0, 1024, 604);
    NSLog(@"%i", index);
    if(index == 0)
    {
        //We're moving to the first, animate the back button to be hidden.
        [UIView animateWithDuration:0.5 animations:^
        {
            self.backButton.alpha = 0.f;
        } completion:^(BOOL finished){
            self.backButton.hidden = YES;
        }];
    }
    else if(index == (self.pageFetchController.fetchedObjects.count - 1))
    {
        [UIView animateWithDuration:0.5 animations:^{
            self.nextButton.alpha = 0.f;
        } completion:^(BOOL finished){
            self.nextButton.hidden = YES;
        }];
    }
    else
    {
        BOOL eitherIsHidden = self.nextButton.hidden || self.backButton.hidden;
        if(eitherIsHidden)
        {
            [UIView animateWithDuration:0.5 animations:^{
                if(self.nextButton.hidden)
                {
                    self.nextButton.hidden = NO;
                    self.nextButton.alpha = 1.f;
                }
                if(self.backButton.hidden)
                {
                    self.backButton.hidden = NO;
                    self.backButton.alpha = 1.f;
                }
            }];
        }
    }
    return newVC;
}

基本上,我创建视图控制器,设置它的数据对象,然后根据索引淡出下一个/后退按钮。

委托方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-(void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
    PageDisplayViewController *vc = [previousViewControllers lastObject];
    NSUInteger index = [self.pageFetchController.fetchedObjects indexOfObject:vc.page];

    if (!completed)
    {
        [self.pagePreviewView setCurrentIndex:index];
        NSLog(@"Animation Did not complete, reverting pagepreview");
    }
    else
    {
        PageDisplayViewController *curr = [pageViewController.viewControllers lastObject];
        NSUInteger i = [self.pageFetchController.fetchedObjects indexOfObject:curr.page];
        [self.pagePreviewView setCurrentIndex:i];
        NSLog(@"Animation compeleted, updating pagepreview. Index: %u", i);
    }
}

我仅注意到此问题,因为随机地,我的后退按钮会重新出现在屏幕上。在其中放入一些NSLog()语句后,我注意到我的dataSource方法被调用以索引1,但是没有动画播放或委托被调用。更可怕的是,如果我尝试平移下一页,则会再次调用索引1。

我担心这可能是UIPageViewController的错误。


由于我的第一个答案仍在实现中遇到神秘的崩溃,因此我一直在寻找"足够好"的解决方案,该解决方案较少依赖于有关页面视图控制器(PVC)基本行为的个人假设。这是我设法提出的。

我以前的方法是一种侵入性方法,而不是可接受的解决方案,更多的是替代方法。与其与PVC进行抗争以强迫它执行我认为应该做的事情,不如更好地接受以下事实:

  • UIKit可以多次调用pageViewController:viewControllerBeforeViewController:pageViewController:viewControllerAfterViewController:方法,并且
  • 绝对不能保证其中任何一个都与分页动画相对应,也不能保证在它们之后都调用pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:

这意味着我们不能将之前/之后的方法用作"动画开始"(但是,请注意,didFinishAnimating仍充当"动画结束"事件)。那么,我们如何知道动画确实已经开始呢?

根据我们的需求,我们可能对以下事件感兴趣:

  • 用户开始摆弄页面:
    一个很好的指标是之前/之后的回调,或更准确地说是第一个。

  • 页面翻动手势的第一视觉反馈:
    我们可以在PVC的点击和平移手势识别器的state属性上使用KVO。当观察到UIGestureRecognizerStateBegan值进行平移时,我们可以很确定地看到视觉反馈。

  • 用户通过释放触摸完成拖动页面:
    再次,KVO。当报告UIGestureRecognizerStateRecognized值用于平移或敲击时,这是PVC实际要翻页的时间,因此可以将其用作"动画开始"。

  • UIKit启动分页动画:
    我不知道该如何获得直接反馈。

  • UIKit结束了分页动画:
    小菜一碟,只听pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:

  • 对于KVO,只需抓住PVC的手势识别器,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @interface MyClass () <UIGestureRecognizerDelegate>
    {
        UIPanGestureRecognizer* pvcPanGestureRecognizer;
        UITapGestureRecognizer* pvcTapGestureRecognizer;
    }
    ...
    for ( UIGestureRecognizer* recognizer in pageViewController.gestureRecognizers )
    {
        if ( [recognizer isKindOfClass:[UIPanGestureRecognizer class]] )
        {
            pvcPanGestureRecognizer = (UIPanGestureRecognizer*)recognizer;
        }
        else if ( [recognizer isKindOfClass:[UITapGestureRecognizer class]] )
        {
            pvcTapGestureRecognizer = (UITapGestureRecognizer*)recognizer;
        }
    }

    然后将您的类注册为state属性的观察者:

    1
    2
    3
    4
    5
    6
    7
    8
    [pvcPanGestureRecognizer addObserver:self
                              forKeyPath:@"state"
                                 options:NSKeyValueObservingOptionNew
                                 context:NULL];
    [pvcTapGestureRecognizer addObserver:self
                              forKeyPath:@"state"
                                 options:NSKeyValueObservingOptionNew
                                 context:NULL];

    并实现通常的回调:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    - (void)observeValueForKeyPath:(NSString *)keyPath
            ofObject:(id)object
            change:(NSDictionary *)change
            context:(void *)context
    {
        if ( [keyPath isEqualToString:@"state"] && (object == pvcPanGestureRecognizer || object == pvcTapGestureRecognizer) )
        {
            UIGestureRecognizerState state = [[change objectForKey:NSKeyValueChangeNewKey] intValue];
            switch (state)
            {
                case UIGestureRecognizerStateBegan:
                // trigger for visual feedback
                break;

                case UIGestureRecognizerStateRecognized:
                // trigger for animation-begin
                break;

                // ...
            }
        }
    }

    完成后,请不要忘记取消订阅这些通知,否则您的应用程序可能会泄漏并发生奇怪的崩溃:

    1
    2
    3
    4
    [pvcPanGestureRecognizer removeObserver:self
                                 forKeyPath:@"state"];
    [pvcTapGestureRecognizer removeObserver:self
                                 forKeyPath:@"state"];

    那就是所有人!


    我已经尝试过您的解决方案,它几乎可以正常工作,但是仍然存在一些问题。最好的解决方案是添加方法

    1
     - (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers

    从iOS 6开始可用,并且是必需的。如果不执行它,这些手势可能会出现问题。实施它有助于解决大部分问题。


    请首先查看我的其他答案,这个答案有严重的缺陷,但我将其留在此处,因为它可能仍会帮助某人。

    首先,免责声明:以下解决方案是HACK。它确实可以在我测试过的环境中工作,但不能保证它可以在您的环境中工作,也不能保证下次更新不会破坏它。因此,请谨慎操作。

    TL; DR:获取UIPageViewControllerUIPanGestureRecognizer并劫持其委托调用,但仍将其转发到原始目标。

    较长版本:

    我在以下问题上的发现:iOS 6中随附的UIPageViewController在行为上与iOS 5中的不同,因为即使没有任何打开的页面,它也可能在其数据源上调用pageViewController:viewControllerBeforeViewController:(阅读:未识别到点击,滑动或有效的方向匹配摇摄)。当然,这打破了我们以前的假设,即之前/之后的调用等效于"动画开始"触发器,并且始终跟随对委托的pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:调用。 (最终,这是一个大胆的假设,但我想我并不孤单。)

    我发现,当页面视图控制器上的默认UIPanGestureRecognizer开始识别最终与控制器方向不匹配的摇动手势时,可能会发生对数据源的额外调用(例如,横向分页PVC)。有趣的是,在我的环境中,总是遇到"之前"方法,而不是"之后"方法。其他人则建议干扰手势识别器的代表,但是这对我的描述不起作用,因此我继续进行实验。

    终于我找到了解决方法。首先,我们获取页面视图控制器的平移手势识别器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @interface MyClass () <UIGestureRecognizerDelegate>
    {
        UIPanGestureRecognizer* pvcPanGestureRecognizer;
        id<UIGestureRecognizerDelegate> pvcPanGestureRecognizerDelegate;
    }
    ...
    for ( UIGestureRecognizer* recognizer in pageViewController.gestureRecognizers )
    {
        if ( [recognizer isKindOfClass:[UIPanGestureRecognizer class]] )
        {
            pvcPanGestureRecognizer = (UIPanGestureRecognizer*)recognizer;
            pvcPanGestureRecognizerDelegate = pvcPanGestureRecognizer.delegate;
            pvcPanGestureRecognizer.delegate = self;
            break;
        }
    }

    然后,在我们的类中实现UIGestureRecognizerDelegate协议:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
    {
        if ( gestureRecognizer == pvcPanGestureRecognizer &&
            [pvcPanGestureRecognizerDelegate respondsToSelector:@selector(gestureRecognizer:shouldReceiveTouch:)] )
        {
            return [pvcPanGestureRecognizerDelegate gestureRecognizer:gestureRecognizer
                                                   shouldReceiveTouch:touch];
        }
        return YES;
    }

    - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
        shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
    {
        if ( gestureRecognizer == pvcPanGestureRecognizer &&
            [pvcPanGestureRecognizerDelegate respondsToSelector:@selector(gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)] )
        {
            return [pvcPanGestureRecognizerDelegate gestureRecognizer:gestureRecognizer
                   shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer];
        }
        return NO;
    }

    - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
    {
        if ( gestureRecognizer == pvcPanGestureRecognizer &&
            [pvcPanGestureRecognizerDelegate respondsToSelector:@selector(gestureRecognizerShouldBegin:)] )
        {
            return [pvcPanGestureRecognizerDelegate gestureRecognizerShouldBegin:gestureRecognizer];
        }
        return YES;
    }

    显然,这些方法没有做任何明智的事情,它们只是将调用转发给原始委托人(确保实际上实现了它们)。尽管如此,这种转发似乎足以使PVC表现出来,并且在不需要时不调用数据源。

    此变通办法为我解决了在运行iOS 6的设备上的问题。使用iOS 6 SDK编译但部署目标为iOS 5的代码已经在5.x设备上完美运行,因此不需要在此进行修复,但根据我的测试也没有任何伤害。

    希望有人觉得这有用。


    试试这个...

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
        - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
     {  

            for (UIGestureRecognizer *gr in pageViewController.gestureRecognizers) {
                    if([gr isKindOfClass:[UIPanGestureRecognizer class]])
                    {
                        UIPanGestureRecognizer *pgr = (UIPanGestureRecognizer*)gr;
                        CGPoint velocity = [pgr velocityInView:pageViewController.view];
                        BOOL verticalSwipe = fabs(velocity.y) > fabs(velocity.x);
                        if(verticalSwipe)
                            return nil;
                    }
                }
        ....
    }