从UIPageViewController中移除视图控制器

50

很奇怪这里没有一种直截了当的方法来实现这个。考虑以下情况:

  1. 您拥有一个包含1个页面的页面视图控制器。
  2. 添加另一页(总共2页)并滚动到该页面。
  3. 我想要的是,当用户向后滚动到第一页时,第二页现在已被删除和卸载,并且用户无法再向后滑动到该页面。

我试过在过渡完成后将视图控制器作为子视图控制器移除,但它仍然让我向后滚动到空页面(它不会“调整”页面视图大小)

我想做的事情是否有可能?


1
实际上有一个简单的方法,只需要重新设置数据源,参见user1459524的答案。 - malhal
12个回答

132

虽然这里提供的答案都很有用,但是这里有另一种解决问题的方法:

UIPageViewController 导航到了错误的页面(使用滚动转换样式)

当我第一次搜索这个问题的答案时,我的搜索方式让我找到了这个问题,而不是我刚刚链接的那个问题,所以我感到有义务发布一个回答,将这个问题链接到另一个问题,并稍作说明。

该问题被 matt 在这里描述得非常好:

这实际上是 UIPageViewController 中的一个 bug。它只在滚动样式(UIPageViewControllerTransitionStyleScroll)下发生,而且只在调用 setViewControllers:direction:animated:completion: 并传入 animated:YES 后才会发生。因此有两种解决方法:

不要使用 UIPageViewControllerTransitionStyleScroll。

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

为了清楚地看到这个 bug,请先调用 setViewControllers:direction:animated:completion:,然后在界面上(作为用户)手动向左(后退)导航到前一个页面。您将会跳转到错误的页面,而不是真正意义上的前一个页面,而是在调用 setViewControllers:direction:animated:completion: 时所处的页面。

这个 bug 的原因似乎是,在使用滚动样式时,UIPageViewController 做了一些内部缓存。因此,在调用 setViewControllers:direction:animated:completion: 后,它未能清除其内部缓存。它认为自己知道前一个页面是什么。因此,当用户向左导航到前一个页面时,UIPageViewController 无法调用数据源方法 pageViewController:viewControllerBeforeViewController:,或者使用当前的 view controller 调用了错误的方法。

这是一个很好的描述,虽然不完全是这个问题,但非常接近。请注意,如果您使用 setViewControllersanimated:NO,则每次用户手势滑动时都会强制 UIPageViewController 重新查询其数据源,因为它不再“知道自己在哪里”或下一个视图控制器在哪里。

然而,这对我并没有起作用,因为有时我需要以编程方式一个动画来移动 PageView。

所以,我的第一个想法是使用动画调用 setViewControllers,然后在完成块中再次调用该方法,使用当前显示的任何视图控制器,但不带动画。因此,用户可以滑动,很好,但然后我们再次调用该方法以重置页面视图。

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

UIPageViewController queuingScrollView:中的断言失败...

由于不知道具体原因,我回溯了一下,最终采用了Jai的答案作为解决方案,创建了一个全新的UIPageViewController,并将其推送到UINavigationController中,然后弹出旧的UIPageViewController。 总体来说很丑陋,但它可以工作 - 大部分时间都可以。 我发现仍然偶尔会从UIPageViewController中获取Assertion Failures,例如:

在-[UIPageViewController queuingScrollView:didEndManualScroll:toRevealView:direction:animated:didFinish:didComplete:]中的断言失败, /SourceCache/UIKit_Sim/UIKit-2380.17/UIPageViewController.m:1820 $1 = 154507824 No view controller managing visible view >

应用程序会崩溃。 为什么呢? 搜索后,我找到了这个其他问题,特别是上面提到的已接受的答案,该答案支持我最初的想法,即仅调用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上的并发用户界面构建(可以在此处通过开发者帐户(here)获得)。现在我想起来了,尝试修改数据源对象以使UIKit对象(比如UIPageViewController)依赖于次要队列可能会导致一些严重的崩溃。

我从未见过特别记录的是,动画的完成块是在次要队列而不是主队列上执行的。所以UIPageViewController吵闹并给出断言失败的原因既是因为当我最初尝试在setViewControllers animated:YES的完成块中调用setViewControllers animated:NO时,又是因为我现在只是使用UINavigationController将其推送到新的UIPageViewController(但再次在setViewControllers animated:YES的完成块中进行)是因为所有这些都发生在次要队列上。

那就是为什么上面那段代码完美地运行,因为你从动画完成块中出来,然后将其发送回主队列,这样您就不会与UIKit交叉。 真是太聪明了。

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

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


1
我想补充一下,我正在使用一个带有相当重量级视图的UIPageViewController(包含各种滚动和集合视图)。虽然这个解决方案确实减少了这些崩溃,但并没有完全消除它们。使用断点,我发现视图控制器开始自动滚动过程和完成块被调用之间存在一些延迟。如果在此期间触摸视图,则应用程序将崩溃。覆盖hitTest无效,所以我只是做了[AppDelegate myapp].window.userInteractionEnabled = NO,并在完成块中重新启用用户交互。 - DivideByZer0
1
@VanDuTran 两个问题:你正在运行哪个版本的iOS,如果你只调用setViewControllers并且使用animated:NO,没有使用完成块会发生什么?这样做会“破坏缓存”吗? - Matt Mc
1
@VanDuTran 很高兴你解决了它。而且,也许苹果已经修复了这个问题!那将是很棒的。 :) - Matt Mc
1
@MattMc,你确定在“__block YourSelfClass *blocksafeSelf = self;”中使用_block是合适的吗?应该使用__weak。 - malex
@malex 感谢您指出这一点。是的,__weak 在这里更加合适。这两个关键字有着不同的用途,我刚才为自己澄清了一下。使用 __weak 可以避免潜在的保留循环问题。再次感谢! - Matt Mc
显示剩余6条评论

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:,它应该按预期工作。

1
可能通过在 if (finished)else 块中添加 completion(finished); 来改进,以确保调用您的完成块。 - Ned
@Ned 谢谢!我现在已经修改了代码,以涵盖那种情况。 - Lukas Kalinski

5

maq说的对。如果你正在使用滚动转换,在UIPageViewController中删除子视图控制器并不会防止用户导航到已删除的“页面”重新出现在屏幕上。如果您感兴趣,这里是我从UIPageViewController中删除子视图控制器的方法。

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

从UIPageViewController的childViewControllers属性中删除视图控制器deleteVC,但如果用户导航到它,则仍然出现在屏幕上。
除非有比我更聪明的人找到了一个优雅的解决方案,否则这里有一个变通方法(这是一种hack--所以你必须问问自己是否真的需要从UIPageViewController中删除页面)。
这些说明假定一次只显示一页。
用户点击表示她想要删除页面的按钮后,在使用setViewControllers:direction:animated:completion:方法导航到下一个或前一个页面。当然,您还需要从数据模型中删除页面的内容。
接下来(这是个hack),创建和配置一个全新的UIPageViewController,并将其加载到前景中(即在其他UIPageViewController之前)。确保新的UIPageViewController开始显示与先前显示完全相同的页面。您的新UIPageViewController将从数据源获取新的视图控制器。
最后,卸载并销毁位于后台的UIPageViewController。
总之,maq问了一个非常好的问题。不幸的是,我没有足够的声望点来赞同这个问题。啊,敢于梦想...总有一天我会拥有15个声望点。

尝试实现这个功能,但是新的UIPageViewController在添加到视图层次结构并调用容器控制器方法以及setViewControllers后不会显示页面,只有在开始使用手势滑动后才会填充页面内容。你遇到过类似的情况吗? - Matt Mc
这解决了我完全关闭PageViewController的问题。谢谢! - brasskazoo

4

我将在此放置这个答案作为我未来的参考,如果有帮助的话——我最终做的是:

  1. 删除该页面并前往下一页
  2. setViewControllers的完成块中,使用修改后的数据(删除项)创建/初始化一个新的UIPageViewController,并且不带动画地推送它,因此屏幕上不会发生任何变化(我的UIPageViewController包含在UINavigationController内部)
  3. 推送新的UIPageViewController后,获取UINavigationController的viewControllers数组的副本,删除倒数第二个视图控制器(即旧的UIPageViewController)
  4. 没有步骤4 - 完成!

1
亲爱的上帝,我花了近8个小时来解决一个场景,在这个场景中我需要动态地改变UIPageViewController的页面!你在这里提供的解决方案终于起作用了 - 希望我能给你点赞多次!:D - Matt Mc
现在你明白我为什么把它发布在这里作为我的参考了,哈哈...很高兴它也帮助到了别人! - Jai Govindani

3

我自己也在学习这个,所以需要保留一定的怀疑态度,但据我所知,您需要更改页面视图控制器的数据源,而不是删除视图控制器。页面视图控制器中显示的页面数量由其数据源决定,而不是由视图控制器决定。


正确的做法是,只需设置一个新的数据源,删除的控制器就会消失。 - malhal
为了保持相同的数据源,请将其设置为nil,然后再设置回原始值。 - N Brown

2
为了改进这个问题,你应该在设置视图控制器之前检测页面是否正在滚动。
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)
            })
        }
    })
}

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

}

-1

我曾遇到类似的情况:我想让用户能够从UIPageViewController中“轻敲并删除”任何页面。经过一番尝试,我发现了比上述方法更简单的解决方案:

捕获“崩溃页面”的页面索引。 检查“崩溃页面”是否为最后一页。 - 如果是最后一页,我们需要向左滚动(如果我们有页面“A B C”并删除C,我们将滚动到B)。 - 如果不是最后一页,我们将向右滚动(如果我们有页面“A B C”并删除B,我们将滚动到C)。 暂时跳转到“安全”位置(理想情况下是最终位置)。使用animated: NO使其瞬间发生。 UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1]; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil]; 从模型/数据源中删除所选页面。 现在,如果它是最后一页: - 调整您的模型以选择左侧的页面。 - 获取左侧的视图控制器,请注意,这与步骤3中检索的视图控制器相同。 - 在从数据源中删除页面后执行此操作非常重要,因为它会刷新它。 - 这次使用animated: YES跳转。 jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1]; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil]; 如果它不是最后一页: - 调整您的模型以选择右侧的页面。 - 获取右侧的视图控制器,请注意,这不是您在步骤3中检索的视图控制器。在我的情况下,您将看到它是dyingPageIndex处的一个,因为已经从模型中删除了崩溃页面。 - 再次强调,在从数据源中删除页面后执行此操作非常重要,因为它会刷新它。 - 这次使用animated: YES跳转。 jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil]; 就这样!这在XCode 6.1.1中效果很好。
完整代码如下。此代码位于我的UIPageViewController中,并通过从要删除的页面调用的委托进行调用。在我的情况下,不允许删除第一页,因为它包含与其余页面不同的内容。当然,您需要进行替换:
  • 你的UIViewController:代表你个人页面的类。
  • 你的总页面数FromModel:代表你模型中页面总数。
  • [YourModel deletePage:]:代码删除正在死亡的页面,从你的模型中删除选定组。

    - (void)deleteAViewController:(id)sender {
        YourUIViewController *dyingGroup = (YourUIViewController *)sender;
        NSUInteger dyingPageIndex = dyingGroup.pageIndex;
        // 检查是否为最后一页,因为它是一个特殊情况。
        BOOL isTheLastPage = (dyingPageIndex >= YourTotalNumberOfPagesFromModel.count);
        // 进行临时跳转 - 确保使用animated:NO使其立即跳转
        UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
        [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];
        // 现在从模型中删除选定组,并设置目标
        [YourModel deletePage:dyingPageIndex];
        if (isTheLastPage) {
            // 现在跳到确定的控制器。这次我们正在重新加载数据源,因此使用了animated:YES
            jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
            [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil];
        } else {
            // 现在跳到确定的控制器。这次重新加载数据源。这次我们使用animated:YES
            jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex];
            [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
        }
    }
    

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

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

我没有完全有时间阅读以上评论,但这对我有效。基本上,我删除了数据(在我的情况下是用户),然后移动到它之前的页面。像魔术一样运作。希望这可以帮助那些寻找快速解决方案的人。


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