从UIPageViewController中删除视图控制器

时间:2013-01-08 17:05:02

标签: iphone objective-c ios cocoa-touch

奇怪的是,没有直接的方法来做到这一点。请考虑以下情形:

  1. 您有一个页面视图控制器,有1页。
  2. 添加另一个页面(共2个)并滚动到它。
  3. 我想要的是当用户滚动回第一页时,第二页现在已被删除并取消分配,用户再也无法刷回该页面。
  4. 我已经尝试在转换完成后将视图控制器移除为子视图控制器,但它仍然允许我滚动回空页面(它不会“调整大小”页面视图)

    我想做的是什么?

12 个答案:

答案 0 :(得分:122)

虽然这里的答案都是提供信息的,但还有一种处理问题的替代方法,如下所示:

UIPageViewController navigates to wrong page with Scroll transition style

当我第一次寻找这个问题的答案时,我在搜索中的措辞让我对这个问题感到伤心,而不是我刚刚联系到的那个,所以我觉得有义务发一个答案链接对于这个问题,现在我已经找到了它,并且还详细说明了一点。

matt here很好地描述了这个问题:

  

这实际上是UIPageViewController中的一个错误。它仅与滚动样式(UIPageViewControllerTransitionStyleScroll)一起发生   在调用setViewControllers后:direction:animated:completion:with   动画:YES。因此有两种解决方法:

     

不要使用UIPageViewControllerTransitionStyleScroll。

     

或者,如果你调用setViewControllers:direction:animated:completion :, use   只有动画:没有。

     

要清楚地看到错误,请致电   setViewControllers:direction:animated:completion:然后,在   interface(作为用户),向左(向后)导航到前一页   手动。您将导航回错误的页面:不是前面的页面   页面,但你所在的页面   setViewControllers:direction:animated:completion:被调用。

     

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

这是一个很好的描述,不是这个问题中提到的问题,而是非常接近。请注意,如果您使用setViewControllers执行animated:NO,则会强制UIPageViewController在下次用户通过手势进行平移时重新查询其数据源,因为它不再知道它在哪里是"或者当前视图控制器旁边有哪些视图控制器。

然而,这对我来说并不起作用,因为有时我需要以编程方式使用围绕移动PageView。

所以,我的第一个想法是用动画调用setViewControllers,然后在完成块中再次使用现在显示的任何视图控制器调用该方法,但没有动画。因此用户可以平移,罚款,但之后我们再次调用该方法以使页面视图重置。

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

  

***断言失败 - [UIPageViewController queuingScrollView:...

我不知道为什么会发生这种情况,我回溯并最终开始使用Jai's answer作为解决方案,创建一个全新的UIPageViewController,将其推送到UINavigationController,然后弹出旧的。毛,但它的确有效 - 主要是。我一直在发现我还是偶尔会从UIPageViewController中得到Assertion Failures,就像这样:

  

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

应用程序崩溃了。为什么?好吧,搜索,我发现我提到的这个other question,特别是提倡我最初想法的accepted answer,只是简单地调用setViewControllers: animated:YES,然后一旦完成调用{{} 1}}使用相同的视图控制器来重置UIPageViewController,但它有缺少的元素:在主队列上调用该代码!这是代码:

setViewControllers: animated:NO

哇!这实际上对我有意义的唯一原因是因为我观看了WWDC 2012 Session 211,在iOS上构建并发用户界面(可用here开发帐户)。我现在回想一下,尝试修改UIKit对象(如UIPageViewController)所依赖的数据源对象,并在辅助队列中执行它可能会导致一些令人讨厌的崩溃。

我从未见过特别记录的内容,但现在必须假设是这样,并且读取,是动画的完成块是在辅助队列而不是主队列上执行的。因此,当我最初尝试在__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 }); } }]; 的完成块中调用setViewControllers animated:NO并且现在我只是使用UINavigationController来推送新的UIPageViewController时,UIPageViewController发出嘎嘎声并给出断言失败的原因(但是,在setViewControllers animated:YES的完成块中再次执行此操作)是因为它发生在该辅助队列中。

这就是为什么那段代码完美运行的原因,因为你来自动画完成块并将其发送回主队列,因此你不必与UIKit交叉。辉煌。

无论如何,想要分享这个旅程,万一有人遇到这个问题。

编辑:Swift版本here,如果有人感兴趣的话。

答案 1 :(得分:9)

总结Matt Mc的答案很好,下面的方法可以添加到UIPageViewController的子类中,这样就可以使用setViewControllers:direction:animated:completion:因为如果bug不存在就打算使用它。

- (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:在实现此方法的类/子类上,它应该按预期工作。

答案 2 :(得分:6)

maq是对的。如果您正在使用滚动转换,则从UIPageViewController中删除子视图控制器不会阻止删除的“页面”在用户导航到屏幕时返回。如果您有兴趣,这是我从UIPageViewController中删除子视图控制器的方法。

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

查看控制器deleteVC将从UIPageViewController的childViewControllers属性中删除,但如果用户导航到该屏幕,它仍会显示在屏幕上。

直到比我更聪明的人找到一个优雅的解决方案,这是一个解决方法(这是一个黑客 - 所以你必须问自己是否真的需要从UIPageViewController中删除页面)。

这些说明假设一次只显示一页。

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

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

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

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

答案 3 :(得分:5)

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

  1. 删除页面并前进到下一页
  2. setViewControllers的完成块中,我创建/初始化了一个带有修改数据的新UIPageViewController(删除了项目),并在没有动画的情况下推送它,因此屏幕上没有任何变化(我的UIPageViewController包含在其中)一个UINavigationController)
  3. 在推送新的UIPageViewController之后,获取UINavigationController的viewControllers数组的副本,删除倒数第二个视图控制器(这是旧的UIPageViewController)
  4. 没有第4步 - 完成!

答案 4 :(得分:3)

为了改善这一点。您应该在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)
            })
        }
    })
}

答案 5 :(得分:2)

当您在viewControllers之间尝试在滑动手势动画期间更改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) {
    }

}

答案 6 :(得分:1)

我只是自己学习这个,所以请耐心等待,但据我所知,你需要更改pageviewcontroller的数据源,而不是删除viewcontroller。 pageviewcontroller中显示的页数由其数据源确定,而不是由viewcontrollers决定。

答案 7 :(得分:0)

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

  1. UIVageController子类实例已从UIPageViewController中删除,方法是将其从模型中删除,并将UIPageViewController的viewControllers属性设置为不同的ViewController。这足以完成这项工作。无需在该控制器上执行任何控制器包含代码
  2. ViewController不见了,但我仍然可以通过在我的情况下向右滑动滚动到它。
  3. 我的问题是这个。我正在为UIPageViewController显示的ViewController添加一个自定义手势识别器。这个识别器作为一个强大的属性保存在也拥有UIPageViewController的控制器上。
  4. FIX:在失去对被取消的ViewController的访问权限之前我确保正确清理了它使用的所有内存(dealloc)并删除了手势识别器
  5. 您的里程可能会有所不同,但我认为这个解决方案没有意义,当某些东西无法正常工作时我首先怀疑我的代码:)

答案 8 :(得分:0)

我有类似的情况,我希望用户能够“点击并删除”UIPageViewController中的任何页面。在玩了一下之后,我发现了一个比上述更简单的解决方案:

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

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

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

    • 调整模型以选择左侧的页面。
    • 在左侧获取viewcontroller,请注意,这与您在步骤3中检索到的相同。
    • 在您从数据源中删除页面后执行此操作非常重要,因为它会刷新页面。
    • 跳转到它,这次是动画:是。

      jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
      [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil];
      
  6. 如果不是最后一页:

    • 调整模型以选择右侧的页面。
    • 在右侧获取viewcontroller,请注意,这不是您在步骤3中检索到的那个。在我的情况下,您将看到它是dyingPageIndex中的那个,因为已经从模型中删除了死亡页面。
    • 同样,在您从数据源中删除页面后执行此操作非常重要,因为它会刷新页面。
    • 跳转到它,这次是动画:是。

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

  8. 以下完整代码。此代码位于我的UIPageViewController中,并通过要删除的页面中的委托进行调用。在我的情况下,不允许删除第一页,因为它包含与其余页面不同的内容。当然,你需要替换:

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

答案 9 :(得分:0)

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

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

答案 10 :(得分:0)

它在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
    }
}

这里有延迟功能

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)
}

答案 11 :(得分:0)

我们在选项卡更改时崩溃。因此,我得到的一个解决方案是选项卡的didChangeSelect禁用了用户吸引力。