UIPageViewController使用滚动过渡样式导航到错误的页面

49
我的UIPageViewController在iOS 5中运行良好。但是在iOS 6推出后,我想使用新的滚动转换样式(UIPageViewControllerTransitionStyleScroll)代替页面翻页样式。这导致我的UIPageViewController出现问题。
它工作得很好,除了在我调用setViewControllers:direction:animated:completion:之后的那一刻。然后,用户手动滚动一页后,我们得到了错误的页面。这里有什么问题?

似乎在9.3中完美运行!如果您仍然感兴趣,请尝试在9.3上测试一下,看看在您这边的表现如何。谢谢。 - mramosch
1
@mramosch,我在iOS 11上也遇到了这个问题。 - Rish K
8个回答

81

我解决这个 bug 的方法是,在完成时创建一个块,该块将设置相同的视图控制器但没有动画。

__weak YourSelfClass *blocksafeSelf = self;     
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:^(BOOL finished){
            if(finished)
            {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [blocksafeSelf.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:NULL];// bug fix for uipageview controller
                });
            }
        }];

1
这两个setViewControllers的调用是否像设置UIPageViewController页面零时那样存在内存泄漏问题(https://dev59.com/U2_Xa4cB1Zd3GeqPxBag)?不仅会出现内存泄漏,而且您还可以看到越来越多的视图控制器添加到UIPageViewController的NSArray *childViewControllers只读属性中。有什么解决方法吗? - matths
1
这个问题在iOS 7.0.3中仍然存在,因此仍然需要一个解决方法。从苹果的角度来看,缓存视图控制器的持久性可能不是一个错误。 - bilobatum
9
天啊,这似乎在iOS 8中仍然存在。而且解决方法似乎不再起作用。还有其他人遇到这个问题吗? - Obiwahn
2
仍然在 iOS 9.2 中。 - Sakiboy
1
这个 bug 在 iOS 12.1 中仍然存在,但我只能在 iPhone(真机和模拟器)上复现,而在 iPad(真机和模拟器)上无法复现。 - EdFunke
显示剩余9条评论

64

这实际上是UIPageViewController中的一个缺陷。它仅在滚动样式(UIPageViewControllerTransitionStyleScroll)下发生,并且仅在使用animated:YES调用setViewControllers:direction:animated:completion:之后才会出现。因此有两种解决方法:

  1. 不要使用UIPageViewControllerTransitionStyleScroll。

  2. 或者,如果您调用setViewControllers:direction:animated:completion:,则仅使用animated:NO

为了清楚地看到错误,请调用setViewControllers:direction:animated:completion:,然后在界面(作为用户)手动向左(返回)导航到前一页。您将导航回错误的页面:不是前一页,而是在调用setViewControllers:direction:animated:completion:时所在的页面。

该错误的原因似乎是,在使用滚动样式时,UIPageViewController会进行某种内部缓存。因此,在调用setViewControllers:direction:animated:completion:之后,它未能清除其内部缓存。它认为自己知道前一页是什么。因此,当用户向左导航到前一页时,UIPageViewController 未调用数据源方法pageViewController:viewControllerBeforeViewController:,或者使用错误的当前视图控制器进行调用。

我发布了一个视频,清楚地演示如何看到这个错误:

http://www.apeth.com/PageViewControllerBug.mov

编辑此错误可能会在iOS 8中被修复

编辑关于此错误的另一个有趣解决方法,请参见此答案:https://dev59.com/B2Up5IYBdhLWcg3wXGsZ#21624169


漏洞仍然存在于iOS7中。 - atrebbi
11
这个漏洞在iOS8中仍然存在。 - Alexander Polovinka
6
是的,Bug仍然存在,将动画设置为“否”没有任何效果。 - Pahnev
1
在iOS 8.2中,将动画设置为“无”似乎对我有用。 - csotiriou
1
iOS 15.1 - 该漏洞仍然存在。将动画设置为false可以解决这个问题。 - Dmitry
显示剩余4条评论

3
这里是我整理的一个“不完美”的要点,它包含了一个UIPageViewController的替代方案,但它没有苹果实现中的内部缓存功能(即阿尔茨海默症)。

这个类并不完整,但在我的情况下(即水平滚动)它是有效的。


感谢Paul创建AlzheimerPageViewController - 它作为PageViewController的替代品非常出色,并修复了在iOS8中似乎仍然存在的此错误。 - Sean Dev
Paul:干得好!!非常感谢你。这个 bug 在 iOS 8.4 中仍然存在,你真的为我节省了很多时间。 - stefat
在9.3中似乎完美运行!有没有之前遇到过问题的人可以试试在9.3上的表现如何,请?谢谢。 - mramosch

1
截至iOS 12,原问题中描述的问题似乎已经得到了几乎修复。我来到这个问题是因为我在我的特定设置中遇到了这个问题,其中它仍然发生,因此这里使用了“几乎”一词。
我遇到这个问题的设置是: 1)通过深度链接打开应用程序 2)基于链接,应用程序必须切换到特定选项卡并通过推送打开其中给定的项目 3)只有当目标选项卡未被用户先前选择(以便UIPageViewController应该动画到该选项卡时)且setViewControllers:direction:animated:completion:具有animated = true时才会出现所述问题。 4)在推送返回到包含UIPageViewController的视图控制器后,后者被发现是一团糟 - 它呈现完全错误的视图控制器,尽管调试显示逻辑层面上一切都很好。
我认为问题的根源是我在调用setViewControllers:direction:animated:completion:之后非常快地推送视图控制器,以至于UIPageViewController没有机会完成某些操作(可能是动画、缓存或其他操作)。
只需通过延迟我的编程导航在UI中给UIPageViewController一些空闲时间即可。
 DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) { ... } 

对我来说解决了这个问题。同时也使得链接项的编程式打开在视觉上更加友好。

希望这能帮助到处于类似情况的人。


感谢您的贡献! - matt

0

因为pageviewVC在滑动时调用多个childVC。但我们只需要最后一个可见的页面。

在我的情况下,当改变pageView时,我需要改变分段控件的索引。

希望这能帮助到某些人 :)

extension ViewController: UIPageViewControllerDelegate {
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        guard let pageView = pageViewController.viewControllers?.first as? ChildViewController else { return }
        segmentedControl.set(pageView.index)
    }
}

0

在 Swift 中另一个简单有效的解决方法:重置 UIPageViewController 的数据源。这样可以清除缓存并避免该 bug。下面是一个直接进入页面而不破坏后续滑动的方法。在下面的代码中,m_pages 是你的视图控制器数组,我将在下面展示如何找到 currPage(当前页面的索引)。

func goToPage(_ index: Int, animated: Bool)
{
    if m_pages.count > 0 && index >= 0 && index < m_pages.count && index != currPage
    {
        var dir: UIPageViewController.NavigationDirection
        if index < currPage
        {
            dir = UIPageViewController.NavigationDirection.reverse
        }
        else
        {
            dir = UIPageViewController.NavigationDirection.forward
        }

        m_pageViewController.setViewControllers([m_pages[index]], direction: dir, animated: animated, completion: nil)
        delegate?.tabDisplayed(sender: self, index: index)
        m_pageViewController.dataSource = self;
    }
}

如何找到当前页面:
var currPage: Int
{
    get
    {
        if let currController = m_pageViewController.viewControllers?[0]
        {
            return m_pages.index(of: currController as! AtomViewController) ?? 0
        }

        return 0
    }
}

0

这个 bug 在 iOS9 中仍然存在。我正在使用与 George Tsifrikas 上面发布的相同解决方法,但是是 Swift 版本:

pageViewController.setViewControllers([page], direction: direction, animated: true) { done in
    if done {
        dispatch_async(dispatch_get_main_queue()) {
            self.pageViewController.setViewControllers([page], direction: direction, animated: false, completion: {done in })
        }
    }
}

没有机会获得9.3之前的设备 - 但在第二代iPad2(不是Air2 !!!)上,使用iOS 9.3,在所有可能的配置中一切都按预期工作。请参见我对已接受答案的评论... - mramosch
有没有之前遇到过问题的人可以在9.3上试一下看看情况如何?谢谢。 - mramosch

-5

声明:

看起来苹果已经注意到开发者在使用UIPageViewController时,将其应用于远超苹果最初设计选择的应用范围之外。PVC通常被用于在结构化环境中以编程方式跳转到随机位置,而不是以手势驱动的线性方式。因此,他们增强了UIPageViewController的实现,该类现在调用了DataSource回调函数。

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

在UIPageViewController上设置新的contentViewController之后

[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];

即使动画翻页暗示了线性进度,例如在书籍或PDF等连续页面的页面层次结构中。尽管我怀疑从HIG的角度来看,苹果不太喜欢看到PVC被用于这种方式,但它并不会破坏向后兼容性,这是一个简单的修复,所以他们最终做到了。实际上,这只是两个DataSource方法之一的另一个调用,在线性环境中,页面(ViewControllers)已经被缓存以供以后使用,因此绝对是不必要的。
然而,即使这种增强可能非常方便某些用例,该类的初始行为也不应被视为错误。许多开发人员在其他SO帖子中也指责UIPageViewController的错误行为,这更强调了其设计、目的和功能的普遍误解。
在这个伟大的设施中,我决定不删除我的最初的“论文”,其中清楚地解释了PVC的机制以及为什么他的假设是错误的,他必须在这里处理一个错误。这对于任何其他努力实现UIPageViewController的开发人员也可能有用!

翻译后的回答:

反复阅读了所有的回答 - 包括被接受的回答 - 只剩下一件事要说了...

UIPageViewController 的设计绝对是完美无缺的,你提交的所有解决方法都是为了规避假定的错误,因为你在一开始就把它搞砸了!

根本没有错误! 你只是在与框架作斗争。我会解释原因!


现在有很多关于页码和索引的讨论!控制器对这些概念一无所知!它唯一知道的是:它正在显示某些内容(顺便提一下,这些内容由您作为数据视图控制器提供),并且它可以执行类似于翻页的右/左动画。

pageViewController的世界里,只存在一个当前SPACE(我们称之为这个名称,以避免与页面和索引混淆)。

当你最初设置一个pageViewController时,它只关心这个特定的SPACE。只有在你开始滑动它的视图时,它才开始询问它的DataSource,在发生左/右翻转时它应该显示什么。当你向左滑动时,PVC首先询问BEFORE-SPACE,然后再询问AFTER-SPACE;如果你向右滑动,那么它的运作方式正好相反。

在完成动画后(PVC的视图显示一个新的SPACE),PVC将此SPACE视为宇宙的新中心,并在此过程中向DataSource询问其仍不知道的内容。如果向右完成一次转弯,它想了解新的AFTER空间;如果向左完成一次转弯,它则要求一个新的BEFORE空间。
在完成向右转弯后,旧的BEFORE空间(来自动画之前)完全过时,并尽快被释放。旧的center现在是新的BEFORE,以前的AFTER是新的center。一切都向右移动了一步。
所以 - 不要谈论“哪一页”或“任何索引” - 只是简单地说,是否有一个BEFOREAFTER空格。如果您在DataSource回调之一中返回NIL,则PVC只是 假设它处于您的SPACES范围内的一个极端位置。如果您对两个都返回NIL 回调,它会认为它正在显示唯一的SPACE,并且永远不会再次调用DataSource回调!逻辑由您定义!您在代码中定义页面和索引!而不是PVC!

对于类的用户,有两种与PVC交互的方式。

  • 一个平移手势,指示是否需要转到BEFORE / AFTER空间
  • 一种方法 - 即setViewControllers:direction:animated:completion:

这个方法与平移手势完全相同。您正在指示方向(例如UIPageViewControllerNavigationDirectionBackward / Forward)进行动画 - 如果有意图的话 - 换句话说就是 -> 去到BEFOREAFTER...

再次强调 - 不提及索引、页码等....!!!

这只是以编程方式实现手势相同的方法!当在第一次向右移动后向左移动时,PVC会通过显示旧内容来正确执行。请记住 - 它只是以结构化的方式显示您提供的内容 - 这是设计上的'单页翻转'!!!

这就是翻页的概念 - 或者如果您更喜欢这个术语,可以称之为BOOK!

仅仅因为您在提交第一页后提交第八页而搞砸了它,并不意味着PVC会关心您对书籍工作方式的扭曲看法。您的应用程序用户也是如此。向右翻页再向左翻页应该肯定会回到原始页面 - 如果使用动画完成。而且,由于您找到了解决方案,所以需要您纠正错误。不要把责任推给UIPageViewController。它正在完美地执行其工作!

只需问自己 - 您是否会在PAGE-CURL动画中做同样的事情? 不会吗?那么,在SCROLL动画中也不应该这样做!动画翻页只是翻页!无论哪种模式!如果您决定将书的第2页到第7页撕下来,那没问题!但是,除非您告诉它事情已经改变,否则不要指望UIPageViewController发明不存在的第7页。


如果你真的想要实现一个不协调的跳转到其他地方,那么最好不要使用动画!在大多数情况下,这样做可能不太优雅,但是它是可行的...

而且 PVC 甚至可以很好地配合使用!当没有动画跳转到一个新的 SPACE 时,它会在后面进一步询问 BEFOREAFTER 控制器。因此,您的应用程序逻辑可以跟上 PVC 的步伐...

但是使用动画时,您总是在传达 - 移动到前/后一个空间 (BEFORE-AFTER)。因此,在动画翻页时,PVC 根本不需要再次询问已知道的空间!

如果你想在从右侧动画翻页到第一页后向左翻回看到第七页 - 那么我会说 - 这绝对是你自己的问题!


如果您正在寻找比接受的答案中的“completion-block” hack更好的解决方案(因为使用它,您会在可能根本不会再用到的情况下提前做一些工作),请使用手势识别器委托。

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer

如果您真的想要返回到第7页并且不需要动画,请在此处设置您的PVC的DataViewController,DataSource将会在BEFOREAFTER时被请求,您可以提交任何您喜欢的页面!通过一个标志或者ivar,当您从第1页跳转到第8页时,您应该已经储存好了这个标志,这样就不会有问题了...

当人们不断抱怨PVC中的一个错误 - 当它应该只做1次翻页时却做了2次翻页 - 就指向这篇文章。

同样的问题 - 在转换手势中触发未动画化的setViewControllers:方法将导致完全相同的混乱。你认为你设置了新的中心 - 数据源被要求提供新的BEFORE - AFTER数据控制器 - 你重置了索引计数... - 好吧,这似乎没问题...

但是 - 在所有这些操作之后,PVC结束了它的转换/动画,并想知道下一个(对它来说仍然未知的)dataViewController(BEFOREAFTER),并且还会触发DataSource。这是完全合理的!它需要知道它在它小小的BEFORE - CENTER - AFTER世界中的位置,并为下一次翻页做好准备。

但是你的程序逻辑将另一个index++计数添加到它的逻辑中,突然间就有了2次翻页!!!而这与你认为的位置相差1个。

而且必须考虑到这一点!而不是UIPageViewController!!!


这就是 DataSourceProtocol 只有两个方法的精髓所在!它想要尽可能地通用,让你有足够的空间和自由定义自己的逻辑,而不被困在别人的特殊想法和用例中!逻辑完全取决于你。只有因为像这样的函数存在,你才能更好地实现自己的特定功能。
- (DataViewController *)viewControllerAtIndex:(NSUInteger)index storyboard:(UIStoryboard *)storyboard position:(GSPositionOfDataViewController)position;
- (NSUInteger)indexOfViewController:(DataViewController *)viewController;

在云中所有复制/粘贴的示例应用程序中,并不意味着您必须吃那些预先烹制好的食物!您可以按照自己的方式进行扩展!只需查看上面 - 在我的签名中,您将找到一个'position:'参数!我将其扩展以后知道完成页面翻转是向左还是向右。因为委托不幸地只告诉您是否完成了翻转!它没有告诉您方向!但这有时对于索引计数很重要,具体取决于您的应用程序的需要...

尽情发挥吧 - 它们是您的...

愉快的编码!!!


很抱歉我直接使用了一些严厉的措辞 - 没有冒犯的意图 - 真的... ;-) - 人们必须先理解UIPageViewController的真正工作方式和设计用途,然后再进行抱怨和提交radars。 - mramosch
顺便问一下,你提到的那个在iOS 6之后被修复的错误是什么?如果你不介意抱怨一下的话。我真的很好奇——它是在这里提到的还是在另一篇文章中?能给我一个链接吗? - mramosch
1
“正在展示的行为今天仍然是一样的”不是这样的。我有原始测试项目。该错误确实发生在iOS 6中,但是相同的代码链接并针对iOS 10运行时表现不同(并且正确)。看,这真的是一个错误,被苹果公司承认并修复了。请把你的长篇文章拿走。你是错的。你当时不在那里,你不知道。 - matt
嗨,Matt,谢谢你的时间!所以,为了澄清一下——你是说在iOS10中(并且精确地重复你在视频中做的步骤)在编程方式下设置您的contentViewController(animated:YES)到第8页后,您现在(通过滑动手势)返回到第7页,因为-> 经过Apple的错误修复,现在两个DataSource回调(BEFORE-AFTER)都会被调用,现在您终于有了钩子,在BEFORE回调中提交第7页,而不必使用任何其他形式的附加函数调用?这正确吗? - mramosch
1
@matt 您链接的示例项目在iOS 11上没有问题,但缓存错误仍未完全修复。当我修改您的示例以便从模态视图控制器调用jumpTo8并将[self dismissViewControllerAnimated:YES completion:nil];插入到jumpTo8的第一行时,向后滑动时会出现先前可见的页面(而不是第7页)。无论如何,此情况可以通过使用animated:NO调用setViewControllers来解决,因为动画也不会可见。 - Sebastian Kirsche
显示剩余2条评论

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