49

奇怪的是,没有直接的方法可以做到这一点。考虑以下场景:

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

转换完成后,我尝试将视图控制器作为子视图控制器删除,但它仍然让我滚动回空白页面(它不会“调整”页面视图的大小)

我想做的事可能吗?

4

12 回答 12

129

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

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

当我第一次搜索这个问题的答案时,我的搜索方式让我陷入了这个问题,而不是我刚刚链接到的那个问题,所以我觉得有义务发布一个链接到另一个问题的答案,既然我已经找到了,也详细说明了一点。

马特在这里很好地描述了这个问题:

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

不要使用 UIPageViewControllerTransitionStyleScroll。

或者,如果您调用 setViewControllers:direction:animated:completion:,则仅使用动画:NO。

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

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

这是一个很好的描述,不是这个问题中提到的问题,但非常接近。请注意,如果您这样做setViewControllersanimated:NO将强制 UIPageViewController 在下次用户使用手势进行平移时重新查询其数据源,因为它不再“知道它在哪里”或哪些视图控制器在其当前视图控制器旁边.

但是,这对我不起作用,因为有时我需要使用动画以编程方式移动 PageView。

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

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

*** 断言失败 -[UIPageViewControllerqueuingScrollView: ...

不知道为什么会发生这种情况,我回溯并最终开始使用Jai 的答案作为解决方案,创建一个全新的 UIPageViewController,将其推送到 UINavigationController,然后弹出旧的。恶心,但它有效 - 主要是。我一直发现我仍然偶尔会从 UIPageViewController 收到断言失败,比如这个:

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

应用程序崩溃。为什么?好吧,搜索,我发现了我在上面提到的另一个问题,特别是提倡我最初想法的公认答案setViewControllers: animated:YES,即简单地调用,然后在完成调用后立即setViewControllers: animated:NO使用相同的视图控制器重置 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
                });
            }
        }];

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

我从未见过特别记录的,但现在必须假设是这种情况并继续阅读的是,动画的完成块是在辅助队列上执行的,而不是在主队列上执行的。因此,当我最初尝试调用setViewControllers animated:NO完成块时,setViewControllers animated:YES以及现在我只是使用 UINavigationController 推送新的 UIPageViewController 时,UIPageViewController 发出尖叫并给出断言失败的原因(但再次在完成中执行它) ) 的块setViewControllers animated:YES是因为这一切都发生在那个辅助队列上。

这就是为什么上面的那段代码可以完美运行的原因,因为您来自动画完成块并将其发送回主队列,这样您就不会与 UIKit 交叉流。杰出的。

无论如何,想分享这个旅程,以防有人遇到这个问题。

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

于 2013-06-26T21:16:41.990 回答
8

总结 Matt Mc 的出色回答,可以将以下方法添加到 UIPageViewController 的子类中,这样就可以使用 setViewControllers:direction:animated:completion:,因为它的目的是在不存在错误的情况下使用。

- (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:,它就会按预期工作。

于 2014-08-28T12:51:13.260 回答
5

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

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

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

在比我聪明的人找到一个优雅的解决方案之前,这里有一个解决方法(这是一个 hack——所以你必须问自己是否真的需要从 UIPageViewController 中删除页面)。

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

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

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

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

无论如何,maq 提出了一个非常好的问题。不幸的是,我没有足够的声望点来支持这个问题。啊,敢做梦……总有一天我会有15点声望。

于 2013-02-13T06:44:10.460 回答
4

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

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

我自己只是在学习这一点,所以持保留态度,但据我了解,您需要更改 pageviewcontroller 的数据源,而不是删除 viewcontroller。pageviewcontroller 中显示的页面数量取决于其数据源,而不是 viewcontrollers。

于 2013-01-09T00:47:36.687 回答
2

为了改善这一点。您应该在 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)
            })
        }
    })
}
于 2015-04-16T12:44:33.093 回答
1

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

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

}
于 2015-06-26T21:41:18.463 回答
-1

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

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

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

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

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

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

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

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

    • 调整您的模型以选择右侧的页面。
    • 获取右侧的视图控制器,注意这不是您在步骤 3 中检索到的那个。在我的例子中,您会看到它是位于 DyingPageIndex 的那个,因为死页已经从模型中删除。
    • 同样,在您从数据源中删除页面之后执行此操作很重要,因为它会刷新它。
    • 跳到它,这次是动画:是的。

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

完整代码如下。此代码在我的 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];
        }
    }
    
于 2015-02-16T05:34:43.883 回答
-1
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];

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

于 2016-09-18T19:02:52.943 回答
-1

它在 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)
}
于 2016-10-05T12:06:02.390 回答
-1

我们在标签更改时崩溃了。所以我得到的一个解决方案是标签的 didChangeSelect 禁用了用户吸引力。

于 2019-10-16T13:33:19.623 回答