UIPageViewController'错误的解决方法

19
当我在UIPageViewController的转场动画之外更快地导航时,我会收到“'Unbalanced calls to begin/end appearance transitions for <MyDataViewController>'” 的错误提示,并且其中一个视图在横向模式下不会显示,直到我尝试翻页为止。
有人有解决这个错误的想法吗?

请发布您的viewWill/DidAppear方法。 - Martol1ni
  • (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.pageNumber = self.pageNumber; self.pageHTML = self.pageHTML; }
其中,pageHTML会将一个HTML字符串加载到一个子视图UIWebView中。
- Basem Saadawy
2
看起来不错。大多数“不平衡的调用”是因为在上一个视图控制器初始化完成之前,新的视图控制器被推送或呈现出来了。 - Martol1ni
10个回答

29

以上的回答是正确的,但我认为比必要的更详细,而且食谱很有帮助。所以这是对我有效的东西:

在设置并调用pageViewController的视图控制器中,声明:

@property (assign)              BOOL pageIsAnimating;

在viewDidLoad中:

    pageIsAnimating = NO;
添加这个:
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers {
    pageIsAnimating = YES;
}

并且添加几行到:

- (void)pageViewController:(UIPageViewController *)pageViewController
    didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers
   transitionCompleted:(BOOL)completed {
    if (completed || finished)   // Turn is either finished or aborted
        pageIsAnimating = NO;
    ...
}
手势被抑制,通过拒绝提供视图控制器信息来实现:

手势被抑制,通过拒绝提供视图控制器信息来实现:

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
   viewControllerAfterViewController:(UIViewController *)viewController {
    if (pageIsAnimating)
        return nil;
    ...
    return after;
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
  viewControllerBeforeViewController:(UIViewController *)viewController {
    if (pageIsAnimating)
        return nil;
    ...
    return before;
}

哦,而且方向更改会重置该标志:

- (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController
               spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation {
    pageIsAnimating = NO;
    ...
}

这只能在iOS 6上运行,因为-pageViewController:willTransitionToViewControllers:在此之前不可用。 - nschmidt
你的代码在iOS6上运行得非常完美,但是在iOS5中什么可以替代willTransitionToViewControllers? - Grace
此解决方案仅适用于iOS6,而我的应用程序支持iOS5。 - Basem Saadawy

9

按照以下步骤解决:
1- 声明一个标志来指示动画是否已经完成:

BOOL pageAnimationFinished;

2- 在viewDidLoad中将此标志设置为true:

pageAnimationFinished = YES;

3- 禁用pageViewController的tapGesture,并将panGestureRecognizer委托给“self”:

for (UIGestureRecognizer * gesRecog in self.pageViewController.gestureRecognizers)
{
    if ([gesRecog isKindOfClass:[UITapGestureRecognizer class]])
        gesRecog.enabled = NO;
    else if ([gesRecog isKindOfClass:[UIPanGestureRecognizer class]])
        gesRecog.delegate = self;
}

4- 通过以下手势识别器委托方法允许/禁止panGestureRecognizer:

-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] && ([gestureRecognizer.view isEqual:self.view] || [gestureRecognizer.view isEqual:self.pageViewController.view]))
    {
        UIPanGestureRecognizer * panGes = (UIPanGestureRecognizer *)gestureRecognizer;
        if(!pageAnimationFinished || (currentPage < minimumPage && [panGes velocityInView:self.view].x < 0) || (currentPage > maximumPage && [panGes velocityInView:self.view].x > 0))
            return NO;
        pageAnimationFinished = NO;
    }
    return YES;
}

5- 添加以下pageViewController委托方法:

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
    pageAnimationFinished = YES;
}

你不应该更改UIPageViewController手势的委托,因为它们依赖于私有实现来正常工作。 - Elland
你应该使用委托。最佳解决方案在这里https://dev59.com/LWYr5IYBdhLWcg3w--0I#22869135。 - SmileBot
#smileBot 我的应用支持 iOS 5,而(pageViewController:willTransitionToViewControllers:)仅在 iOS 6 及以上版本可用。 - Basem Saadawy

6

Basem Saadawy的回答很好,但它有一些缺陷。

实际上,委托的gestureRecognizerShouldBegin:可能会在没有进一步动画开始的情况下调用。如果你通过垂直手指移动来启动手势,并且它的水平偏移量不足以启动动画(但足以启动gestureRecognizerShouldBegin:),那么我们的变量pageAnimationFinished将被设置为 NO ,没有真正的动画。因此,pageViewController:didFinishAnimating:永远不会被调用,你将得到当前页面被冻结,无法更改。

这就是为什么将 NO 分配给此变量的更好位置是手势识别器的操作方法,并检查其速度和平移(我们只关心水平方向)。

因此,最后的步骤是:

1)声明一个实例变量(标志):

BOOL pageAnimationFinished;

2) 设置其初始值

- (void)viewDidLoad
{
    [super viewDidLoad];
    ...
    pageAnimationFinished = YES;
}

3) 为平移手势识别器指定委托和自定义操作

for (UIGestureRecognizer * gesRecog in self.pageViewController.gestureRecognizers)
{
    if ([gesRecog isKindOfClass:[UIPanGestureRecognizer class]])
    {
        gesRecog.delegate = self;
        [gr addTarget:self action:@selector(handlePan:)];
    }
}

3') 当手势在水平方向上的位移较大且手指正在水平移动时,动画才真正开始。
我猜测内部识别器由 UIPageViewController 分配的操作也是使用相同的逻辑。

- (void) handlePan:(UIPanGestureRecognizer *)gestureRecognizer
{
    if (pageAnimationFinished && gestureRecognizer.state == UIGestureRecognizerStateChanged)
    {
        CGPoint vel = [gestureRecognizer velocityInView:self.view];
        CGPoint tr = [gestureRecognizer translationInView:self.view];
        if (ABS(vel.x) > ABS(vel.y) && ABS(tr.x) > ABS(tr.y))
            pageAnimationFinished = NO; // correct place
    }
}

4) 如果动画未完成,则禁止手势。

-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] && ([gestureRecognizer.view isEqual:self.view] || [gestureRecognizer.view isEqual:self.pageViewController.view]))
    {
        UIPanGestureRecognizer * panGes = (UIPanGestureRecognizer *)gestureRecognizer;
        if(!pageAnimationFinished || (currentPage < minimumPage && [panGes velocityInView:self.view].x < 0) || (currentPage > maximumPage && [panGes velocityInView:self.view].x > 0))
            return NO;
    }
    return YES;
}

5) 动画已完成

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
    pageAnimationFinished = YES;
}

我使用它玩得太多了,似乎这是一个不错的解决方案,而且运作良好。

提前致谢。你提到的问题在我这里没有出现,因为持有 UIPageViewController 的视图上没有附加 UIPanGestureRecognizer。无论如何,你的建议是一个好的修改。 - Basem Saadawy

6

这是使用委托的快速版本:

添加以下代码(确保在头文件或类扩展中包含UIPageViewControllerDelegate,并将self.pageViewController.delegate = self;赋值):

- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers {
   self.pageAnimationFinished = NO;
}

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed {
    self.pageAnimationFinished = YES;
}

然后检查self.pageAnimationFinished,如果它等于NO,则返回nil。

更详细的解释:

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed

我们可以使用UIPageViewControllerDelegate中的这个委托方法来知道翻转或滑动页面的动画何时结束。我们只需要像这样实现即可:
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed {
    pageAnimationFinished = YES;
}

那么,在你的代码中,只需返回nil即可。
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(PageViewController *)viewController

并且

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(PageViewController *)viewController

pageAnimationFinished == NO 时,一定要在动画时将 pageAnimationFinished 设置为 NO。最好的方法是使用相反的方式来确定何时进行动画。

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed

即:

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

自从那时候以来,我再也没有看到过这个警告,而且这可以用其他解决方案的三分之一行数完成。而且这个方法更容易理解。


1
好答案。在我看来应该被接受。我稍微编辑了一下你的回答。在我看来,使用getter设置为isAnimating的动画效果更好。这样可以检查索引是否为0或self.isAnimating,并返回nil。这只是一个风格问题。 :) - SmileBot

4
以下是 Bill Cheswick 回答的 Swift 版本(目前排名第一):
添加一个变量来保存当前状态:
var pageIsAnimating = false

设置动画状态:

func pageViewController(pageViewController: UIPageViewController, willTransitionToViewControllers pendingViewControllers: [UIViewController]) {
    self.pageIsAnimating = true
}

func pageViewController(pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    if finished || completed {
        self.pageIsAnimating = false
    }
}

如果当前正在进行动画,请阻止过渡效果:

func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
    if self.pageIsAnimating {
        return nil
    }

    // Your code here
}

func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
    if self.pageIsAnimating {
        return nil
    }

    // Your code here
}

感谢 Bill Cheswick!

1
我可以确认这对我有效,但仅适用于手势驱动的页面转换。这种解决方案的一个副作用是,用户不幸的是不能像手指在边缘轻扫/点击那样快速地翻看所有视图。另外,当使用pageViewController.setViewControllers()以编程方式更改页面时,设置pageIsAnimating函数并不会被调用。但这可以通过在pageViewController.setViewControllers()的完成块中将pageIsAnimating设置为false来减轻。 - SomeGuy

3

我的解决方案是用Swift编写的,简单易懂且有效:

  1. Set pageviewcontroller delegate to your class
  2. Add below code

    extension MyPageVC: UIPageViewControllerDelegate {
    
        func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
            self.view.isUserInteractionEnabled = false
        }
    
        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
            self.view.isUserInteractionEnabled = true
        }
    }
    

1
对我来说,这就是它了。谢谢!+1(Swift 3) - Jiří Zahálka

1
这个怎么样:
- (void)pageViewController:(UIPageViewController*)pgVC willTransitionToViewControllers:(NSArray*)pendingVCs
{
    pgVC.dataSource = nil; // ... to disallow user to change pages until animation completes
}

- (void)pageViewController:(UIPageViewController*)pgVC
        didFinishAnimating:(BOOL)finished
   previousViewControllers:(NSArray*)prevVCs
       transitionCompleted:(BOOL)completed
{
    if(completed || finished) {
        pgVC.dataSource = _pgViewDataSource; // ... to allow user to change pages again
    }
}

0

我不得不将它添加到viewDidAppear:中才能使其工作

    - (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    self.pageAnimationFinished = YES;
}

0

利用您的UIPageViewControllerDelegate方法并设置保护措施,以防止检测到过多页面翻转时创建新的页面视图。

  1. 您可以禁用手势识别器
  2. 在UIView上将"userInteraction"设置为禁用
  3. 在UIPageViewController上维护一个标志,以忽略动画发生时的进一步输入。(关于此选项的警告.. ios5和ios6有不同的确定动画开始时间的方式..)

你能告诉我如何解决这个问题吗?我一直面临这个问题,但是没有找到解决方案。 - Sugan S
你有很多选择,但基本上如果在动画完成之前调用页面翻转(就像快速滑动),则会出现此错误。因此,您要做的是设置代码以阻止/忽略进一步的滑动,直到当前动画完成。我通过在动画期间设置标志来解决它,并且进一步调用页面翻转将简单地被忽略,直到动画完成并清除了标志。这还有助于限制过多的页面翻转请求,并且对操作系统/内存更加简单。 - timzilla
在ContentViewController的viewWillAppear方法中,我检查了标志值以显示视图,但什么也没有发生,我仍然得到空视图。你能帮我吗? - Sugan S

-2

我会尝试在转场时忽略 UIPageViewController 上的手势。


@Basem,你修好了这个问题吗?你能告诉我如何解决它吗?我也遇到了同样的问题,还没有解决方案? - Sugan S
@BasemSaadawy 我有一个PageViewController和ContentViewController,我需要限制视图的加载吗? - Sugan S
或者你能添加一些代码,我正在尝试在viewDidLoad和viewDidUnload中解决问题,但是还没有解决。@BasemSaadawy - Sugan S
我正在使用 UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController 方法。 - Sugan S
@sugan.s 你试过上面的解决方案了吗? - Basem Saadawy
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接