UIPageViewController and "No view controller managing visible view"

In my code I do sometimes "reset" UIPageViewController by using the construct as given below:

id vc = [pageViewController.viewControllers firstObject];
__weak UIPageViewController *pvc = pageViewController;
__weak MyViewController *weakSelf = self; // UIViewController subclass which is hosting pageViewController

pageViewController.dataSource = nil;
[pageViewController setViewControllers: @[ [[UIPageViewController alloc] init] ]
                             direction: UIPageViewControllerNavigationDirectionForward
                              animated: NO completion: ^(BOOL completed) {
     [pvc setViewControllers: @[ vc ] direction: UIPageViewControllerNavigationDirectionForward
          animated: NO completion: ^(BOOL completed) {
               pvc.dataSource = weakSelf;
          }];
}];


This works perfectly almost all the time, but recently, when I was fast swiping pages back and forth, the app has crashed with NSInternalInconsistencyException caused in:

*** Assertion failure in -[UIPageViewController queuingScrollView:didEndManualScroll:toRevealView:direction:animated:didFinish:didComplete:], /SourceCache/UIKit/UIKit-3318.16.14/UIPageViewController.m:1875


The crash was caused by reset code being invoked from viewWillLayoutSubviews method of a view controller which is hosting UIPageViewController. The reason was: "No view controller managing visible view: (here goes my custom view)". This custom view is only created within vc instances - view controllers that are passed to UIPageViewController. So I don't think it's possible for them to exist without this view controller. I am not able to reproduce this issue, but I believe it can happen again, at random.


What can be the real reason for this happening?

Is this a bug in UIKit?

Are there any workarounds?

Accepted Reply

First of all, setViewControllers:direction:animated:completion: is for you to set all the view controllers that will be visible after the animation has finished: sending that an array that consists solely of a freshly allocated UIPageViewController means that the original pageViewController object will just be showing a brand new UIPageViewController and no content view controllers, which I'm pretty sure is very bad and really not what you intended.

The array should consist of whichever view controllers you want to be showing next.


Secondly, is your UIPageViewController set with a page transition style of UIPageViewControllerTransitionStyleScroll?

If so, there is indeed a bug in UIKit that sometimes causes this exception; I *think* it can be prevented by making sure you don't initiate a second call to setViewControllers:direction:animated:completion: until the first has completed, but I've yet to test the theory.

Replies

First of all, setViewControllers:direction:animated:completion: is for you to set all the view controllers that will be visible after the animation has finished: sending that an array that consists solely of a freshly allocated UIPageViewController means that the original pageViewController object will just be showing a brand new UIPageViewController and no content view controllers, which I'm pretty sure is very bad and really not what you intended.

The array should consist of whichever view controllers you want to be showing next.


Secondly, is your UIPageViewController set with a page transition style of UIPageViewControllerTransitionStyleScroll?

If so, there is indeed a bug in UIKit that sometimes causes this exception; I *think* it can be prevented by making sure you don't initiate a second call to setViewControllers:direction:animated:completion: until the first has completed, but I've yet to test the theory.

Well, the intended behavior is to reset UIPageViewController to avoid caching of offscreen view controllers. Sadly I didn't found an official way to invalidate the view controllers that are currently not visible on the screen.


The empty view controller is only set for a while and completion block restores the original view controller, and this mostly behaves as I would expect it to. Of course I don't consider this a beautiful piece of code. I would love to have something like -[UIPageViewController pleaseDoInvalidateOffscreenControllersAndQueryTheDataSourceAgainBecauseIKnowTheStateHasChangedAndThereShouldBeDifferentControllerBeforeTheCurrentOne] in that place.


Yes, UIPageViewController in question is using UIPageViewControllerTransitionStyleScroll. I will set up the test to see if this error can be triggered in controller manner by doing second setViewControllers:animated:completion: before the first is completed. Thanks for this advice.

A test had shown that calling -[UIPageViewController setViewControllers:animated:completion:] when previous, animated transition is not finished, causes this assertion failure in repeatable manner. Knowing the cause it will now be easier to find a workaround.

UIPageViewController in iOS has some bugs.

Use UIScrollView + NSArray of UIViewController instead!

Maybe this will help.


http://weijun.me/post/develop/2015-11-26

id vc = [pageViewController.viewControllers firstObject]; __weak UIPageViewController *pvc = pageViewController; __weak MyViewController *weakSelf = self; // UIViewController subclass which is hosting pageViewController pageViewController.dataSource = nil; [pageViewController setViewControllers: @[ [[UIPageViewController alloc] init] ] direction: UIPageViewControllerNavigationDirectionForward animated: NO completion: ^(BOOL completed) { [pvc setViewControllers: @[ vc ] direction: UIPageViewControllerNavigationDirectionForward animated: NO completion: ^(BOOL completed) { pvc.dataSource = weakSelf; }]; }];

id vc = [pageViewController.viewControllers firstObject]; __weak UIPageViewController *pvc = pageViewController; __weak MyViewController *weakSelf = self; // UIViewController subclass which is hosting pageViewController pageViewController.dataSource = nil; [pageViewController setViewControllers: @[ [[UIPageViewController alloc] init] ] direction: UIPageViewControllerNavigationDirectionForward animated: NO completion: ^(BOOL completed) { [pvc setViewControllers: @[ vc ] direction: UIPageViewControllerNavigationDirectionForward animated: NO completion: ^(BOOL completed) { pvc.dataSource = weakSelf; }]; }];

id vc = [pageViewController.viewControllers firstObject]; __weak UIPageViewController *pvc = pageViewController; __weak MyViewController *weakSelf = self; // UIViewController subclass which is hosting pageViewController pageViewController.dataSource = nil; [pageViewController setViewControllers: @[ [[UIPageViewController alloc] init] ] direction: UIPageViewControllerNavigationDirectionForward animated: NO completion: ^(BOOL completed) { [pvc setViewControllers: @[ vc ] direction: UIPageViewControllerNavigationDirectionForward animated: NO completion: ^(BOOL completed) { pvc.dataSource = weakSelf; }]; }];

ranakaysk Jun 5, 2016 4:25 AM (in response to zzz) id vc = [pageViewController.viewControllers firstObject]; __weak UIPageViewController *pvc = pageViewController; __weak MyViewController *weakSelf = self; // UIViewController subclass which is hosting pageViewController pageViewController.dataSource = nil; [pageViewController setViewControllers: @[ [[UIPageViewController alloc] init] ] direction: UIPageViewControllerNavigationDirectionForward animated: NO completion: ^(BOOL completed) { [pvc setViewControllers: @[ vc ] direction: UIPageViewControllerNavigationDirectionForward animated: NO completion: ^(BOOL completed) { pvc.dataSource = weakSelf; }]; }]; This helped me Show 0 Likes (0) Reply

tanakaysk Jun 5, 2016 4:25 AM (in response to zzz) id vc = [pageViewController.viewControllers firstObject]; __weak UIPageViewController *pvc = pageViewController; __weak MyViewController *weakSelf = self; // UIViewController subclass which is hosting pageViewController pageViewController.dataSource = nil; [pageViewController setViewControllers: @[ [[UIPageViewController alloc] init] ] direction: UIPageViewControllerNavigationDirectionForward animated: NO completion: ^(BOOL completed) { [pvc setViewControllers: @[ vc ] direction: UIPageViewControllerNavigationDirectionForward animated: NO completion: ^(BOOL completed) { pvc.dataSource = weakSelf; }]; }]; This

(in response to zzz) id vc = [pageViewController.viewControllers firstObject]; __weak UIPageViewController *pvc = pageViewController; __weak MyViewController *weakSelf = self; // UIViewController subclass which is hosting pageViewController pageViewController.dataSource = nil; [pageViewController setViewControllers: @[ [[UIPageViewController alloc] init] ] direction: UIPageViewControllerNavigationDirectionForward animated: NO completion: ^(BOOL completed) { [pvc setViewControllers: @[ vc ] direction: UIPageViewControllerNavigationDirectionForward animated: NO completion: ^(BOOL completed) { pvc.dataSource = weakSelf; }]; }];

Afaik you can do this by setting the datasource to nil. This will invalidate the viewcontroller.


dataSource = nil;
dataSource = self;