从UIPageViewController中移除一个视图控制器

很奇怪,没有简单的方法来做到这一点。 考虑以下情况:

  1. 你有一页页面视图控制器。
  2. 添加另一个页面(共2)并滚动到它。
  3. 我想要的是,当用户滚动回到第一页时,第二页现在被移除和释放,并且用户不能再滑回到该页面。

我已经尝试过渡完成后删除视图控制器作为子视图控制器,但它仍然让我滚动回空页面(它不“调整”页面视图)

我想做什么?

虽然这里的答案都是信息性的,但还有一个处理这个问题的替代方法,在这里给出:

UIPageViewController导航到滚动转换样式的错误页面

当我第一次search这个问题的答案的时候,我的search方式让我对这个问题感到困惑,而不是我刚刚链接的那个,所以我觉得有义务发表一个与这个问题相关的答案,现在我已经find了,并且稍微详细一点。

这个问题在这里用亚光描述得非常好:

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

不要使用UIPageViewControllerTransitionStyleScroll。

或者,如果您调用setViewControllers:direction:animated:completion:,则只使用animation:NO。

为了清楚地看到bug,调用setViewControllers:direction:animated:completion:然后,在界面(以用户身份)中,手动向左(后退)导航到前一页。 您将导航回错误的页面:根本不是前面的页面,而是在setViewControllers:direction:animated:completion:被调用时所在的页面。

这个错误的原因似乎是,当使用滚动样式时,UIPageViewController会执行某种内部caching。 因此,在调用setViewControllers:direction:animated:completion:之后,它无法清除其内部caching。 它认为它知道前一页是什么。 因此,当用户向左导航到前一页时,UIPageViewController无法调用dataSource方法pageViewController:viewControllerBeforeViewController:,或者使用错误的当前视图控制器调用它。

这是一个很好的描述,不是这个问题中提到的问题,而是非常接近的问题。 注意,如果你使用animated:NO setViewControllers animated:NO你会强制UIPageViewController重新查询其数据源,下一次用户平息手势,因为它不再“知道它在哪里”或什么视图控制器旁边它当前的视图控制器。

然而,这不适合我,因为有时候我需要以animation的方式编程地移动PageView。

所以,我的第一个想法是用animation调用setViewControllers ,然后在完成块中再次调用该方法与任何视图控制器现在显示,但没有animation。 所以用户可以平移,很好,但是我们再次调用方法来获取页面视图来重置。

不幸的是,当我尝试,我开始从页面视图控制器奇怪的“断言错误”。 他们看起来像这样:

***声明失败 – [UIPageViewController queuingScrollView:…

我不知道到底为什么会发生这种情况,于是我回过头来,最终开始使用Jai的答案作为解决scheme,创build一个全新的UIPageViewController,将其推送到UINavigationController上,然后popup旧的。 毛,但它的作品 – 主要是。 我一直在发现我仍然偶尔从UIPageViewController断言失败,就像这样:

***断言失败 – [UIPageViewController queuingScrollView:didEndManualScroll:toRevealView:方向:animation:didFinish:didComplete:],/SourceCache/UIKit_Sim/UIKit-2380.17/UIPageViewController.m:1820 $ 1 = 154507824没有视图控制器pipe理可见视图>

而应用程序崩溃。 为什么? 那么search,我发现了我提到的另一个问题 ,特别是提出我最初想法的接受的答案 ,简单地调用setViewControllers: animated:YES ,然后一旦完成调用setViewControllers: animated:NO查看控制器重置UIPageViewController,但它有缺less的元素:调用该代码回主队列! 代码如下:

 __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 }); } }]; 

哇! 这对我来说唯一的理由是因为我已经观看了WWDC 2012会议211,在iOS上构build并发用户界面( 这里提供了一个开发账户)。 我记得现在试图修改UIKit对象(如UIPageViewController)所依赖的数据源对象,并在辅助队列上进行操作可能会导致一些令人讨厌的崩溃。

我从来没有见过特别logging,但现在必须假设是这样的情况,并阅读,是一个animation完成块是在一个二级队列,而不是主要的。 所以,UIPageViewController之所以吵闹,是因为当我最初试图调用setViewControllers animated:NOsetViewControllers animated:YES的完成块中,现在我只是简单地使用UINavigationController来推新的UIPageViewController(但是再次,在setViewControllers animated:YES )的完成块中, setViewControllers animated:YES是因为它都发生在该辅助队列上。

这就是为什么那段代码完美地工作,因为你来自animation完成块,并将其发送回主队列,所以你不会通过UIKit跨越stream。 辉煌。

无论如何,要分享这个旅程,以防万一遇到这个问题。

编辑: 这里的 Swift版本,如果任何人有兴趣。

总结马特Mc的伟大的答案,下面的方法可以被添加到UIPageViewController的子类,这样的方式允许使用setViewControllers:方向:animation:完成:因为它打算如果错误不会出现使用。

 - (void) setViewControllers:(NSArray*)viewControllers direction:(UIPageViewControllerNavigationDirection)direction animated:(BOOL)animated completion:(void (^)(BOOL))completion { if (!animated) { [super setViewControllers:viewControllers direction:direction animated:NO completion:completion]; return; } [super setViewControllers:viewControllers direction:direction animated:YES completion:^(BOOL finished){ if (finished) { dispatch_async(dispatch_get_main_queue(), ^{ [super setViewControllers:viewControllers direction:direction animated:NO completion:completion]; }); } else { if (completion != NULL) { completion(finished); } } }]; } 

现在,只需在实现此方法的类/子类上调用setViewControllers:direction:animated:completion:就可以按预期工作。

maq是对的。 如果您正在使用滚动过渡,从UIPageViewController中删除子视图控制器不会阻止删除的“页面”返回到屏幕上,如果用户导航到它。 如果你有兴趣,这是我如何从UIPageViewController中删除子视图控制器。

 // deleteVC is a child view controller of the UIPageViewController [deleteVC willMoveToParentViewController:nil]; [deleteVC.view removeFromSuperview]; [deleteVC removeFromParentViewController]; 

视图控制器deleteVC从UIPageViewController的childViewControllers属性中移除,但如果用户导航到它,仍然显示在屏幕上。

直到比我聪明的人find一个优雅的解决scheme,这是一个解决方法(这是一个黑客 – 所以你必须问自己,如果你真的需要从UIPageViewController删除页面)。

这些说明假定一次只显示一个页面。

用户点击表示要删除页面的button后,使用setViewControllers:direction:animated:completion:方法导航到下一个或上一个页面。 当然,您需要从数据模型中删除页面的内容。

接下来(这里是黑客攻击),创build并configuration一个全新的UIPageViewController并将其加载到前台(即在另一个UIPageViewController的前面)。 确保新的UIPageViewController开始显示与之前显示的完全相同的页面。 新的UIPageViewController将从数据源中获取新的视图控制器。

最后,卸载并销毁在后台的UIPageViewController。

无论如何,马克问一个非常好的问题。 不幸的是,我没有足够的信誉点来投票。 啊,敢于梦想…总有一天我会有15个声望点。

我将这个答案放在这里仅仅是为了我自己的未来,如果它帮助任何人 – 我最终做的是:

  1. 删除页面并进入下一页
  2. setViewControllers的完成块中,我创build/初始化一个新的UIPageViewController与修改的数据(项目删除),并推动它没有animation,所以没有任何改变屏幕上(我的UIPageViewController是包含在一个UINavigationController)
  3. 推入新的UIPageViewController后,获取UINavigationController的viewControllers数组副本,删除倒数第二个视图控制器(这是旧的UIPageViewController)
  4. 没有第四步 – 完成!

为了改善这一点。 你应该在setViewControllers之前检测pageView是否滚动。

 var isScrolling = false func viewDidLoad() { ... for v in view.subviews{ if v.isKindOfClass(UIScrollView) { (v as! UIScrollView).delegate = self } } } func scrollViewWillBeginDragging(scrollView: UIScrollView){ isScrolling = true } func scrollViewDidEndDecelerating(scrollView: UIScrollView){ isScrolling = false } func jumpToVC{ if isScrolling { //you should not jump out when scrolling return } setViewControllers([vc], direction:direction, animated:true, completion:{[unowned self] (succ) -> Void in if succ { dispatch_async(dispatch_get_main_queue(), { () -> Void in self.setViewControllers([vc], direction:direction, animated:false, completion:nil) }) } }) } 

当您尝试在viewControllers之间的滑动手势animation期间更改viewControllers时存在此问题。 为了解决这个问题,我做了一个简单的标志来发现页面视图控制器在转换过程中。

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

而当我试图控制视图控制器,我正在检查是否有过渡进行中。

 - (void)setCurrentPage:(NSString *)newCurrentPageId animated:(BOOL)animated { if (self.pageViewControllerTransitionInProgress) { return; } [self.pageContentViewController setViewControllers:@[pageDetails] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:^(BOOL finished) { } } 

我只是自己学习这个,所以拿一点盐,但从我的理解,你需要改变pageviewcontroller的数据源,而不是删除viewcontroller。 在pageviewcontroller中显示多less页面是由其数据源而不是视图控制器决定的。

其实我想我解决了这个问题。 这是我的应用程序中发生的事情:

  1. 通过从UIPageViewController中移除UIViewController子类实例,将其从模型中移除,并将UIPageViewController的viewControllers属性设置为不同的ViewController。 这足以完成这项工作。 不需要在该控制器上执行任何控制器控制代码
  2. ViewController已经不在了,但是我仍然可以通过向右滑动来查看它。
  3. 我的问题是这个。 我正在向UIPageViewController显示的ViewController添加一个自定义手势识别器。 这个识别器在拥有UIPageViewController的控制器上是一个强大的属性。
  4. 修复: 放弃访问ViewController被解雇之前,我确保我正确地清理了它使用的所有内存(dealloc),并删除了手势识别器
  5. 你的里程可能会有所不同,但我认为这个解决scheme是错误的,当有些东西不能工作时,我首先怀疑我的代码:)

我有类似的情况,我希望用户能够从UIPageViewController“点击和删除”任何页面。 玩了一下之后,我find了比上面描述的更简单的解决scheme:

  1. 捕获“垂死的页面”的页面索引。
  2. 检查“垂死的页面”是否是最后一页。
    • 这很重要,因为如果它是最后一页,我们需要向左滚动(如果我们有页面“ABC”和删除C,我们将滚动到B)。
    • 如果不是最后一页,我们将向右滚动(如果我们有页面“ABC”和删除B,我们将滚动到C)。
  3. 暂时跳到一个“安全”的地方(理想的是最后一个)。 使用animation:NO来让这个瞬间发生。

     UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1]; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil]; 
  4. 从模型/数据源中删除选定的页面。

  5. 现在,如果这是最后一页:

    • 调整您的模型以select左侧的页面。
    • 获取左侧的视图控制器,注意这与步骤3中检索到的相同。
    • 在从数据源中删除页面之后,执行此操作非常重要,因为它会刷新它。
    • 跳转到这个animation:YES。

       jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1]; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil]; 
  6. 在不是最后一页的情况下:

    • 调整模型以select右侧的页面。
    • 得到右侧的viewcontroller,请注意,这不是在第3步中检索到的。在我的情况中,您将看到它是dyingPageIndex中的一个,因为垂死的页面已经从模型中删除。
    • 同样,在从数据源删除页面之后执行此操作也很重要,因为它会刷新页面。
    • 跳转到这个animation:YES。

       jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil]; 
  7. 而已! 这在XCode 6.1.1中运行良好。

下面的完整代码。 这段代码位于我的UIPageViewController中,并通过页面中的委托来调用。 在我的情况下,删除第一页是不允许的,因为它包含与其余页面不同的东西。 当然,你需要replace:

  • YourUIViewController:与您的个人网页的类。
  • YourTotalNumberOfPagesFromModel:包含模型中的页面总数
  • [YourModel deletePage:]用代码从模型中删除垂死的页面

     - (void)deleteAViewController:(id)sender { YourUIViewController *dyingGroup = (YourUIViewController *)sender; NSUInteger dyingPageIndex = dyingGroup.pageIndex; // Check to see if we are in the last page as it's a special case. BOOL isTheLastPage = (dyingPageIndex >= YourTotalNumberOfPagesFromModel.count); // Make a temporary jump back - make sure to use animated:NO to have it jump instantly UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1]; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil]; // Now delete the selected group from the model, setting the target [YourModel deletePage:dyingPageIndex]; if (isTheLastPage) { // Now jump to the definitive controller. In this case, it's the same one, we're just reloading it to refresh the data source. // This time we're using animated:YES jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1]; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil]; } else { // Now jump to the definitive controller. This reloads the data source. This time we're using animated:YES jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex]; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil]; } } 
 NSMutableArray *mArray = [[NSMutableArray alloc] initWithArray:self.userArray]; [mArray removeObject:userToBlock]; self.userArray = mArray; UIViewController *startingViewController = [self viewControllerAtIndex:atIndex-1]; NSArray *viewControllers = @[startingViewController]; [self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil]; 

我没有时间阅读上面的评论,但是这对我有效。 基本上我删除数据(在我的情况下,用户),然后移动到之前的页面。 奇迹般有效。 希望这可以帮助那些寻求快速修复的人。

它在Swift 3.0中做了以下

 fileprivate var isAnimated:Bool = false override func setViewControllers(_ viewControllers: [UIViewController]?, direction: UIPageViewControllerNavigationDirection, animated: Bool, completion: ((Bool) -> Void)? = nil) { if self.isAnimated { delay(0.5, closure: { self.setViewControllers(viewControllers, direction: direction, animated: animated, completion: completion) }) }else { super.setViewControllers(viewControllers, direction: direction, animated: animated, completion: completion) } } extension SliderViewController:UIPageViewControllerDelegate { func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { self.isAnimated = true } func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { self.isAnimated = finished } } 

而这里的延迟function

 func delay(_ delay:Double, closure:@escaping ()->()) { DispatchQueue.main.asyncAfter( deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure) }