pageViewController setViewControllers 因“无效参数不满足:[views count] == 3”而崩溃

Posted

技术标签:

【中文标题】pageViewController setViewControllers 因“无效参数不满足:[views count] == 3”而崩溃【英文标题】:pageViewController setViewControllers crashes with "Invalid parameter not satisfying: [views count] == 3" 【发布时间】:2014-06-02 18:09:43 【问题描述】:

我知道还有其他几个类似的问题,但我找不到我的问题的解决方案。 我使用显示不同 ViewControllers 的 pageViewController。每次 pageViewController 向前移动时,我都会检查 lastPage 的输入。如果错误,pageViewController 应该使用 setViewController 方法返回该页面。没有这种方法一切正常,但如果我尝试使用它,应用程序会崩溃并出现以下异常:

19:48:25.596 Phook[23579:60b] *** Assertion failure in -[_UIQueuingScrollView _replaceViews:updatingContents:adjustContentInsets:animated:], /SourceCache/UIKit_Sim/UIKit-2935.137/_UIQueuingScrollView.m:383  
2014-06-02 19:48:25.600 Phook[23579:60b] *** Terminating app due to uncaught   exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying:   [views count] == 3'

这是我的代码:

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished
   previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed

    if (completed && finished)
    

        RegisterUserPageContent* lastPage = (RegisterUserPageContent*)previousViewControllers[ previousViewControllers.count-1];
        int lastIndex   = lastPage.pageIndex;
        int newIndex    = ((RegisterUserPageContent*)[self.pageViewController.viewControllers objectAtIndex:0]).pageIndex;

        if (newIndex > lastIndex)
           // Moved Forward
            if(!lastPage.testInput)
            
                [self.pageViewController setViewControllers:@[ [self.storyboard instantiateViewControllerWithIdentifier:_pageStoryBoardIDs[0]] ]
                                         direction:UIPageViewControllerNavigationDirectionReverse
                                         animated:YES completion:nil];


            
        
        else
           // Moved Reverse

        
    

正如我已经说过的。我进行了很多搜索并实施了一些解决方案,但没有任何帮助。 谢谢。

【问题讨论】:

【参考方案1】:

我遇到了同样的问题,并且能够使用此答案 https://***.com/a/20973822/3757370 中的提示解决它。只需将 setViewControllers:direction:animated:completion: 代码放在主队列上的 dispatch_async 块内即可为我修复它。对你来说,这看起来像

dispatch_async(dispatch_get_main_queue(), ^
    [self.pageViewController setViewControllers:@[ [self.storyboard instantiateViewControllerWithIdentifier:_pageStoryBoardIDs[0]] ]
                                     direction:UIPageViewControllerNavigationDirectionReverse
                                     animated:YES completion:nil];
);

希望对你有帮助!

【讨论】:

谢谢!我已经看到过这种方式,但我再次尝试了一些属性 - 现在它可以工作了。 这也为我解决了这个崩溃问题,即使代码已经在主队列上执行了 这对我也有用,但很高兴知道当代码已经在主队列上时为什么它实际上有帮助。 警惕这种解决方案。它修复了Invalid parameter not satisfying,但引入了NSInternalInconsistencyException Duplicate states in queue,它更难重现,但仍然会导致足够多的崩溃让用户担心。 你们中的任何更新,这不起作用?【参考方案2】:

遇到了同样的问题。通过在 UIPageViewController 首次加载时显式设置第一个内容控制器来解决它(即:在“viewDidLoad”内部)。

// create first page
UIViewController* firstContentPageController = [self contentControllerAtIndex: 0];
[_paginationController
    setViewControllers: @[firstContentPageController]
    direction: UIPageViewControllerNavigationDirectionForward
    animated: NO
    completion: nil];

其中 'contentControllerAtIndex:' 是一个创建内容控制器的简单辅助方法。我还在两个委托方法中使用它,以便为给定页面返回适当的控制器。

- (UIViewController*)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController 
    NSInteger index = (NSInteger)((MyContentController*)viewController).pageIndex;

    return [self contentControllerAtIndex: index - 1];



- (UIViewController*)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController 
    NSInteger index = (NSInteger)((MyContentController*)viewController).pageIndex;

    return [self contentControllerAtIndex: index + 1];



- (MyContentController*)contentControllerAtIndex: (NSInteger)index 
    if (index < 0 || _pagesContent.count <= index)
        return nil;

    // create view controller
    MyContentController* contentController = [self.storyboard instantiateViewControllerWithIdentifier: @"MyContentController"];
    contentController.pageIndex = index;
    contentController.content = [_pagesContent objectAtIndex: index];

    return contentController;

这样做的原因是 UIPageViewControllerDataSource 协议被设计为仅具有用于拉上一个/下一个内容控制器的方法,而不是在特定索引处拉单个控制器。这是一个奇怪的设计决定,但需要注意的是,您必须手动设置其起始状态,而不是在首次加载组件时由 Cocoa 调用。恕我直言,这里的框架设计很差。

【讨论】:

我已经在我的 viewDidLoad 方法中设置了第一个 ViewController。所以这不是问题。当我稍后尝试使用 setViewContollers 方法时,我的问题首先出现。在 if 语句之后,我尝试跳回之前的页面。但是这里的方法不起作用(不再)。 我通过使用您的代码 sn-p 设置我的第一个视图控制器来修复我的代码。第一个孩子根本没有出现,但我可以滑动到导致这个异常的第二个孩子。我在没有故事板的情况下在代码中做所有事情,因为我的 UIPageViewController 视图是更大 UI 的子视图。【参考方案3】:

嗯,现在是 2018 年,错误仍然存​​在。我尝试了上述所有解决方案,但没有一个对我有用。在多次尝试之后,将动画参数设置为 false,这对我来说是 swift 4

let myViewController : UIViewController = orderedViewControllers[vcIndex]
setViewControllers([myViewController], direction: .forward, animated: false, completion: nil);

然后只需设置自定义过渡即可。希望它可以帮助其他处于相同情况的人。

【讨论】:

dispatch_async 对于 webkit 视图交换控制器来说是不够的。我还不得不关闭动画。【参考方案4】:

Swift 4.2 版本的正确答案。

    DispatchQueue.main.async 
        self.pageViewController?.setViewControllers([self.initialViewControllerAtIndex(index: 0)!], direction: .forward, animated: false, completion: nil)
    

【讨论】:

【参考方案5】:

我有同样的输出错误,即使我的情况并不完全相同。我在UIViewController 内的viewDidAppear(:_) 中调用emailTextField.becomeFirstResponder() 时看到了这个错误UIPageViewController。问题是我正在为UIPageViewControllerUIView 设置动画以在键盘出现时移动一些UI 元素。

在摸索了几个小时后,我发现如果您将调用封装在 DispatchQueue.main.async 块中,它就不再中断。

我的层次结构和进一步解释:

UIViewController:它的底部有两个按钮和一个UIContainerView,其中包含一个UIPageViewController。这个UIViewController 监听NSNotification.Name.UIKeyboardWillChangeFrame 在键盘出现时移动底部的导航按钮(不要试图改变容器的大小,我试过了,结果更糟)。当点击其中一个按钮时,UIViewController 使用所需的视图控制器调用子 UIPageViewController.setViewControllers(_:direction:animated:completion:)

如果您使用animated: false 调用该方法,它会起作用。如果在为键盘更改设置UIView 的动画时为UIViewControllers 的插入设置动画,则会中断。

此外,UIPageViewController 中的每个UIViewController 还监听NSNotification.Name.UIKeyboardWillChangeFrame 以更改包装内容的 UIScrollView 的大小。

嵌入UIPageViewController 的每个UIViewController 中的最终代码如下:

override func viewDidAppear(_ animated: Bool) 
    super.viewDidAppear(animated)
    DispatchQueue.main.async 
        self.emailTextField.becomeFirstResponder()
    


override func viewWillDisappear(_ animated: Bool) 
    super.viewWillDisappear(animated)
    emailTextField.resignFirstResponder()

【讨论】:

【参考方案6】:

我们最近进行了一些调查,发现禁用动画可以防止崩溃。但是我们还是想在切换标签页的时候显示动画,所以我们想办法避免在可能导致崩溃的状态上出现动画。这是解决方案:

let vc = self.orderedViewControllers[index]
self.pageViewController.setViewControllers([vc], direction: direction, animated: !self.pageViewController.children.contains(vc), completion:nil)

以下是调查结果:

    使用childViewControllers 而不是viewControllers 来检查屏幕上的当前视图控制器。 childViewControllers 反映了 UIPageViewController 上的实际视图控制器,而 viewControllers 正是我们设置的/用户刚刚滑动到的视图控制器,它的计数是 1 或 2。

    UIPageViewController 可能会变得陈旧 - 拥有两个以上的子视图控制器,并且无法通过用户滑动手势来移除它们。 调用setViewControllers 会重置正确的状态,但如果有动画就会崩溃。

    我们可以通过在不同标签之间快速点击或按住UIPageViewController同时快速点击不同标签(成功率更高)来重现陈旧状态。看起来复杂的操作会破坏UIPageViewController的内部队列,并且会因为抛出这个错误而崩溃:

    NSInternalInconsistencyException:无效参数不满足:[views count] == 3

    didFinishAnimating回调中调用setViewControllers也会导致崩溃。

【讨论】:

【参考方案7】:

我今天遇到了这个问题。 我在 UIPageController 切换 viewController 时启动动画。

- (void)setViewControllers:(nullable NSArray<UIViewController *> *)viewControllers direction:(UIPageViewControllerNavigationDirection)direction animated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion;

如果animation:YES,程序会崩溃,但如果animation:NO 就可以了。

我想我们不能同时制作两个动画。 但是如果你想要animation:YES,你可以在切换viewControlle之后做你的其他动画,比如之后使用GCD。

【讨论】:

【参考方案8】:

我在将pageViewController.dataSource = nil 设置为在用户滚动到某个页面后停止滚动时遇到了这个问题。

事实证明,解决方案似乎是不使用上述任何异步解决方法。一般来说,您应该尽一切可能避免这些变通办法——它们通常表明您的做法是错误的。

至少对我来说,解决方案是确保在您调用pageViewController.setViewControllers(...)之前调用pageViewController.dataSource = nil

如果你之后nil它,即使在pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool)内部,你也会得到上面提到的异常。

因此,在调用 setViewControllers 之前,请将 dataSource 设置为您想要的值。

【讨论】:

【参考方案9】:

除了

DispatchQueue.main.async 
        self.pageViewController?.setViewControllers([self.initialViewControllerAtIndex(index: 0)!], direction: .forward, animated: false, completion: nil)
    

我还必须将以下代码放在 DispatchQueue.main.async

DispatchQueue.main.async 
           for v in self.pageViewController!.view.subviews 
                if let sub = v as? UIScrollView 
                    sub.isScrollEnabled = enable
                
            

因为我启用/禁用滚动,直到用户完成填写前一页上的值,然后才允许他们转到下一页

【讨论】:

【参考方案10】:

我遇到了同样的问题,但是在主队列上执行还不够。

在其中实例化了多个视图控制器 func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController?

func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?

这些控制器可能会在它们不可见时尝试操纵 UI!在这些控制器中使用 viewDidAppear 将是过于简单的解决方案,无法让它们仅在可见时修改 UI。所以我想出了一个解决方案:

    纯斯威夫特

使用 isInWindow 来检测视图是否可见,因此可以安全地操作 UI。

import UIKit
extension UIViewController 
    var isInWindow: Bool 
        return self.viewIfLoaded?.window != nil
    

    RxSwift + RxCocoa
class MyViewControllerThatMightBePaged 

    var hasAppeared = BehaviorRelay<Bool>(value: false)
    var windowObservation: NSKeyValueObservation?

    override func viewDidLoad() 
        super.viewDidLoad()

        // Do any additional setup after loading the view.

        // If not paged, the View will have a window immediatelly
        hasAppeared.accept(self.parent != nil)
        windowObservation = observe(\.parent, options: [.new], changeHandler:  [weak self] (_, change) in
            self?.hasAppeared.accept(change.newValue != nil)
        )


        hasAppeared.asObservable()
        .distinctUntilChanged()
        .filter($0)
        .take(1)
            .subscribe(onNext:  [weak self] (_) in
                // DO the initial UI manipulating magic
            , onError: nil, onCompleted: nil, onDisposed: nil)
        .disposed(by: disposeBag)
    

    deinit 
        windowObservation?.invalidate()
        windowObservation = nil
    


【讨论】:

【参考方案11】:

我发现在viewDidLoad() 中调用两次方法pageViewController.setViewControllers(...) 会导致这个错误?

【讨论】:

【参考方案12】:

我有同样的错误。除了像其他人建议的那样将它包装在DispatchQueue.main.async 中之外,我还在完成块中添加了一个动画,这似乎导致了问题。

所以我想这是另一件需要注意的事情,以防人们在阅读所有解决方案后仍然摸不着头脑。

【讨论】:

【参考方案13】:

我在重复调用setViewControllers 时遇到了这个崩溃问题,与主线程无关

这是我的背景,在滑动页面内容视图控制器时,我需要更新页面中UISegmentControl的选定段索引,然后选定段索引更改块尝试调用setViewControllers 当然也在主线程中。但是崩溃了!

简单修复:

UIViewController *targetVC = ...;

if (![self.pageVC.viewControllers.firstObject isEqual:targetVC]) 
    [self.pageVC setViewControllers:@[targetVC]
                          direction:UIPageViewControllerNavigationDirectionForward
                           animated:NO
                         completion:nil];

只需尝试检查最新的视图控制器上下文以切断额外的重复调用,解决!希望对您有所帮助!

【讨论】:

以上是关于pageViewController setViewControllers 因“无效参数不满足:[views count] == 3”而崩溃的主要内容,如果未能解决你的问题,请参考以下文章

pageViewController 的视差效果

PageviewController 数据源方法

Swift 中的 PageViewController 当前页面索引

如何在 SwiftUI 中的 PageViewController 中传递一些视图?

移除 PageViewController

如何根据 PageViewController 更改按钮的操作?