pageViewController setViewControllers crashes with

2020-02-07 19:52发布

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.

标签: xcode
9条回答
Summer. ? 凉城
2楼-- · 2020-02-07 20:17

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.

查看更多
Melony?
3楼-- · 2020-02-07 20:17

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()
}
查看更多
Animai°情兽
4楼-- · 2020-02-07 20:18

Swift 4.2 version of the correct answer.

    DispatchQueue.main.async {
        self.pageViewController?.setViewControllers([self.initialViewControllerAtIndex(index: 0)!], direction: .forward, animated: false, completion: nil)
    }
查看更多
狗以群分
5楼-- · 2020-02-07 20:24

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!

查看更多
Deceive 欺骗
6楼-- · 2020-02-07 20:24

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.

查看更多
Juvenile、少年°
7楼-- · 2020-02-07 20:24

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
    }

}
查看更多
登录 后发表回答