很奇怪这里没有一种直截了当的方法来实现这个。考虑以下情况:
- 您拥有一个包含1个页面的页面视图控制器。
- 添加另一页(总共2页)并滚动到该页面。
- 我想要的是,当用户向后滚动到第一页时,第二页现在已被删除和卸载,并且用户无法再向后滑动到该页面。
我试过在过渡完成后将视图控制器作为子视图控制器移除,但它仍然让我向后滚动到空页面(它不会“调整”页面视图大小)
我想做的事情是否有可能?
很奇怪这里没有一种直截了当的方法来实现这个。考虑以下情况:
我试过在过渡完成后将视图控制器作为子视图控制器移除,但它仍然让我向后滚动到空页面(它不会“调整”页面视图大小)
我想做的事情是否有可能?
虽然这里提供的答案都很有用,但是这里有另一种解决问题的方法:
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 调用了错误的方法。
这是一个很好的描述,虽然不完全是这个问题,但非常接近。请注意,如果您使用 setViewControllers
和 animated: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。
setViewControllers
并且使用animated:NO
,没有使用完成块会发生什么?这样做会“破坏缓存”吗? - Matt Mc__weak
在这里更加合适。这两个关键字有着不同的用途,我刚才为自己澄清了一下。使用 __weak
可以避免潜在的保留循环问题。再次感谢! - Matt Mc综合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);
}
}
}];
}
if (finished)
的 else
块中添加 completion(finished);
来改进,以确保调用您的完成块。 - Nedmaq说的对。如果你正在使用滚动转换,在UIPageViewController中删除子视图控制器并不会防止用户导航到已删除的“页面”重新出现在屏幕上。如果您感兴趣,这里是我从UIPageViewController中删除子视图控制器的方法。
// deleteVC is a child view controller of the UIPageViewController
[deleteVC willMoveToParentViewController:nil];
[deleteVC.view removeFromSuperview];
[deleteVC removeFromParentViewController];
我将在此放置这个答案作为我未来的参考,如果有帮助的话——我最终做的是:
setViewControllers
的完成块中,使用修改后的数据(删除项)创建/初始化一个新的UIPageViewController,并且不带动画地推送它,因此屏幕上不会发生任何变化(我的UIPageViewController包含在UINavigationController内部)viewControllers
数组的副本,删除倒数第二个视图控制器(即旧的UIPageViewController)我自己也在学习这个,所以需要保留一定的怀疑态度,但据我所知,您需要更改页面视图控制器的数据源,而不是删除视图控制器。页面视图控制器中显示的页面数量由其数据源决定,而不是由视图控制器决定。
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)
})
}
})
}
- (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) {
}
}
我曾遇到类似的情况:我想让用户能够从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中效果很好。[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];
}
}
它是用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)
}
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];
我没有完全有时间阅读以上评论,但这对我有效。基本上,我删除了数据(在我的情况下是用户),然后移动到它之前的页面。像魔术一样运作。希望这可以帮助那些寻找快速解决方案的人。