40

I know that there are several other questions like this, but i couldn't find a solution for my problem. I use a pageViewController which displays different ViewControllers. Every time the pageViewController moves forward I check the input of the lastPage. If it's wrong the pageViewController should go back to that page by using the setViewController method. Without this method everything works fine but if I try to use it the app crashes with the following exception:

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'

And here's my code:

- (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

        }
    }
}

As I already said. I searched a lot and implemented some solutions but nothing helped. Thanks.

Björn
  • 608
  • 2
  • 7
  • 19

13 Answers13

60

I ran into the same problem, and was able to solve it using a tip from this answer https://stackoverflow.com/a/20973822/3757370. Simply placing the setViewControllers:direction:animated:completion: code inside of a dispatch_async block on the main queue fixed it for me. For you this would look like

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

Hope it helps!

Community
  • 1
  • 1
jpecoraro342
  • 784
  • 6
  • 6
  • Thank you! I've already seen this way but I tried it again an chanced a few attributes - now it works. – Björn Jul 15 '14 at 23:43
  • 1
    this fixed this crash for me as well, even though the code was already being executed on the main queue – Ilias Karim May 12 '15 at 00:25
  • 1
    This worked for me as well, but it would be nice to know why it actually helps when the code is already on the main queue. – Henrik Aug 29 '15 at 15:46
  • 2
    Be wary of this solution. It fixes the `Invalid parameter not satisfying` but introduces `NSInternalInconsistencyException Duplicate states in queue`, which is harder to reproduce but still causes enough crashes for users to be worried about. – Alexandre G Nov 23 '16 at 00:35
  • 1
    any updates from those of you, for which this didn't work? – nr5 Aug 01 '17 at 16:03
9

Ran into this exact same issue. Solved it by setting the first content controller explicitly on the UIPageViewController when it is first loaded (ie: inside 'viewDidLoad').

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

where 'contentControllerAtIndex:' is a simple helper method that creates a content controller. I also use it within the two delegate methods in order to return the appropriate controller for a given page.

- (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;
}

The reason for this is that the UIPageViewControllerDataSource protocol was designed to only have methods for pulling the previous/next content controller, as opposed to pulling a single controller at a particular index. It's an odd design decision, but the caveat is that instead of being called by Cocoa when the component is first loaded you have to manually set its starting state. Poor framework design here IMHO.

Marchy
  • 3,334
  • 5
  • 37
  • 27
  • I already set the first ViewController in my viewDidLoad-method.So that cannot be the problem. My problem first comes up when I try to use the setViewContollers-method later. After an if-statement I try to jump back to an earlier page. But here the method does not work (anymore). – Björn Jun 22 '14 at 00:19
  • I fixed my code by setting my first viewcontroller using your code snippet. The initial child wasn't showing up at all but I could swipe to the second which caused this exception. I'm doing everything in code without a storyboard as my UIPageViewController view is a subview of a larger UI. – cynistersix Mar 08 '15 at 19:37
6

Well it's 2018 and the bug is still around. I tried all the solutions above, but none worked for me. And after many tries, setting the animation parameter to false, was what worked for me in swift 4

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

Then it was just a matter of setting a custom transition. Hope it helps others in the same situation.

kax
  • 61
  • 1
  • 1
5

We did some investigations recently and found disabling the animation would prevent the crash. But we still want to show the animation when switching tabs, so we tried to find a way to avoid the animation on the state that can cause the crash. Here's the solution:

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

Here are the findings:

  1. Use childViewControllers instead of viewControllers to check the current view controllers on the screen. The childViewControllers reflects the actual view controllers on the UIPageViewController, while the viewControllers is just what we set / the view controller that user just swiped to and it's count is 1 or 2.

  2. The UIPageViewController can get stale - having more than two child view controllers and cannot remove them by user swiping gestures. Calling setViewControllers would reset the correct state but it would crash if there's an animation.

  3. We can reporduce the stale state by quickly tapping between different tabs or keep pressing on the UIPageViewController while quickly tapping different tabs at the same time (which has a higher success rate). It seems the complex operations would break the internal queue of the UIPageViewController, and it would crash by throwing this error:

    NSInternalInconsistencyException: Invalid parameter not satisfying: [views count] == 3

  4. Calling setViewControllers in the didFinishAnimating callback would also cause crash.

Bing
  • 351
  • 3
  • 12
4

Swift 4.2 version of the correct answer.

    DispatchQueue.main.async {
        self.pageViewController?.setViewControllers([self.initialViewControllerAtIndex(index: 0)!], direction: .forward, animated: false, completion: nil)
    }
Enea Dume
  • 3,014
  • 3
  • 21
  • 36
2

I had the same output error, even though my case it's not exactly the same. I saw this error while calling emailTextField.becomeFirstResponder() in viewDidAppear(:_) in a UIViewController inside a UIPageViewController. The problem was that I was animating the UIPageViewController parent UIView to move some UI elements when the keyboard appeared.

After scratching my head for a few hours, I found that if you wrap the call in a DispatchQueue.main.async block it no longer breaks.

My hierarchy and further explaination:

UIViewController: it has two buttons at the bottom and a UIContainerView that holds a UIPageViewController. This UIViewController listens to NSNotification.Name.UIKeyboardWillChangeFrame to move the bottom navigation buttons when the keyboard appears (don't try to change the size of the container, I tried that and it broke even more). When one of the buttons is tapped, the UIViewController calls the child UIPageViewController.setViewControllers(_:direction:animated:completion:) with the needed view controller.

If you call that method with animated: false it works. If you animate the insertion of UIViewControllers while animating the UIView for the Keyboard changes, it breaks.

Furthermore, every UIViewController inside the UIPageViewController also listens to NSNotification.Name.UIKeyboardWillChangeFrame to change the size of the UIScrollView that wraps the content.

My final code inside every UIViewController that's embedded inside the UIPageViewController is the following:

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

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    emailTextField.resignFirstResponder()
}
Xavi Moll
  • 247
  • 2
  • 14
1

I get this problem today.
I start an animation when UIPageController switch viewController.

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

Program will crash if animation:YES,but if animation:NO it will be fine.

I guess we cant do two animations as the same time. But if you want animation:YES,you can do your other animation after switch viewControlle's,such as use GCD after.

mag_zbc
  • 6,801
  • 14
  • 40
  • 62
Heisenbean
  • 61
  • 6
1

I found that calling twice the method pageViewController.setViewControllers(...) inside viewDidLoad() leads to this error

0

I was experiencing this problem when setting pageViewController.dataSource = nil to stop scrolling once the user scrolls to a certain page.

Turns out, the solution appears to be to not use any of the above async workarounds. In general you should do everything you can to avoid these kinds of workarounds — they typically show you are holding it wrong.

The solution for me at least is to make sure pageViewController.dataSource = nil is called before you call pageViewController.setViewControllers(...).

If you nil it afterwards, even inside pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool), you will get the exception mentioned above.

So set dataSource to what you want it to be before you call setViewControllers.

Marc Palmer
  • 507
  • 2
  • 9
0

In addition to

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

I had to also place this following code within a DispatchQueue.main.async

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

Because I was enable/disabling scrolling until users completed filling out values on previous pages, before allowing them to go to the next page

Jase Whatson
  • 4,179
  • 5
  • 36
  • 45
0

I had the very same problem, but executing on main queue was not enough.

There are multiple viewcontrollers instantiated in func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController?

and

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

These controllers might try to manipulate UI when they are not visible! Using viewDidAppear would be too easy solution in those controllers, to make them modify UI only when visible. So I came up with a solution:

  1. Pure Swift

Use isInWindow to detect if the view is visible, so safe to manipulate UI.

import UIKit
extension UIViewController {
    var isInWindow: Bool {
        return self.viewIfLoaded?.window != nil
    }
}
  1. 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
    }

}
Géza Mikló
  • 81
  • 1
  • 6
0

I had the same error. In addition to wrapping it in DispatchQueue.main.async like what others suggested, I also had an animation in the completion block that seemed to be causing a problem.

So I guess that's another thing to watch out for, in case people are still scratching their heads after reading all the solutions.

PostCodeism
  • 1,070
  • 1
  • 12
  • 20
0

I encountered this crash issue when calling the setViewControllers repeatedly, not related to main thread.

Here is my background, when swiping the page content view controller, I need to update the selected segment index of UISegmentControl in my page, then the selected segment index changed block try to call the setViewControllers too, of course, in main thread. But crashed!

Easy fix:

UIViewController *targetVC = ...;

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

Just try to check the lastest view controller context to cut off the additional repeat call, solved! Hope it helps!

Itachi
  • 5,777
  • 2
  • 37
  • 69