1

In the iOS 11 app I'm developing (using Swift 4), I need to dynamically add pages to a UIPageViewController. However, in some circumstances, I receive the following crash error (NSInternalInconsistencyException):

2017-11-27 14:55:54.787260+0000 MyApp[380:79434] *** Assertion failure in -[UIPageViewController _flushViewController:animated:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3698.21.8/UIPageViewController.m:2124
2017-11-27 14:55:54.791022+0000 MyApp[380:79434] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Don't know about flushed view <UIView: 0x12dd48ac0; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x1c003c280>>'

I am very new to iOS development, so I'm sorry if the answer is obvious, but I'm having a hard time finding the root cause for the crash. The logged message isn't being very helpful either...

This is the code I'm using to add pages:

//Inflate the view controller
let viewController = newViewController(param: someParam )

//Add it to the view controllers pool
viewControllersOrdered.insert(viewController, at: getInsertionIndex(param: someOtherParam) )

//Add it to the pageViewController
pageViewController.setViewControllers([viewController], direction: .forward, animated: false, completion: nil)

//Force cache to be cleared
pageViewController.dataSource = nil;
pageViewController.dataSource = self;

The getInsertionIndex function is working correctly, that is not the source of the crash.

AmiguelS
  • 805
  • 2
  • 10
  • 28
  • What you're doing is very weird. It is not normal to keep on hand a pool of view controllers with a UIPageViewController. The whole idea is that it has only _one_ view controller. Otherwise, you are wasting memory. The fact that you have to "clear the cache" shows that this is not normal, and it is not going to work; you cannot interfere with the UIPageVC's caching mechanism. You should just implement the delegate methods and let the UIPageViewController do its caching and manage the child view controllers. – matt Nov 27 '17 at 15:13
  • @matt Hi, thanks for replying. I am clearing the cache because when I removed the views, the viewController would still let the user scroll to them, because the getBefore/After methods had already been called. The pageViewController interface really seems to be fighting the dynamic addition/removal of pages. Am I missing something? – AmiguelS Nov 27 '17 at 15:17
  • You are not missing anything at all! You are absolutely right. The problem is that you have chosen a view controller architecture that you must then struggle against. That is a Bad Smell. See my answer below. – matt Nov 27 '17 at 15:18
  • Maybe the answer in this post: [assertion-failure-in-uipageviewcontroller)](https://stackoverflow.com/questions/42833765/assertion-failure-in-uipageviewcontroller) can it help you. If not, please provide more code... And "clear the cache" is nor normal. – Kevinosaurio Nov 27 '17 at 15:21

2 Answers2

6

In the iOS 11 app I'm developing (using Swift 4), I need to dynamically add pages to a UIPageViewController.

It sounds like what you mean is: you do not know what the next / previous view controller will be until the user actually tries to "turn the page". In that case, a scrolling UIPageViewController is not a good fit for your architecture (because, as you have already figured out, it precaches the next and previous pages). You have two choices:

  • Use a page-curl UIPageViewController. It doesn't precache its pages, so you are not called upon to make a decision until the user actually asks to turn the page.

  • Don't use a UIPageViewController at all; use a paging horizontal scroll view and manage the content view yourself.

Also, if you're using a UIPageViewController, in no case should you also be holding a retained list of child view controllers. That is the job of the UIPageViewController. Your job is merely to supply the next or previous view controller, which you should create on demand and at no other time, and release it into the UIPageViewController's sole control.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I know what the previous and next pages are. However, the user can navigate to a different screen were he can add and remove pages from being shown. That's what I mean by dynamically doing adding/removing. – AmiguelS Nov 27 '17 at 15:20
  • That doesn't change my answer. That notion is contrary to the way a scrolling page view controller works. Pick another architecture. – matt Nov 27 '17 at 15:21
  • I do have one other idea. Your child view controllers don't actually configure themselves fully until they come into view; you'll get `viewWillAppear`. So if these are all instances of the same view controller class, you could perform the configuration then. But either way, you need to get rid of the retained list of view controllers; that's just wrong. – matt Nov 27 '17 at 15:22
  • Ok, I'll take a look at the suggestions you provided. Thanks! – AmiguelS Nov 27 '17 at 15:24
  • Over due update: I followed your second suggestion (paging horizontal scroll view) and it's working as intended now. Thank you. – AmiguelS Feb 21 '18 at 14:49
  • @matt can you provide a source for this your last paragraph? _"Also, if you're using a UIPageViewController, in no case should you also be holding a retained list of child view controllers."_ I couldn't see something of that nature in the UIPageViewController documentation. – Patrick Nov 07 '21 at 09:53
2

The main rules of crash-free using UIPageController with scroll transition style:

1) set dataSource before calling setViewControllers method

2) use setViewControllers method without animation (animated: false)

3) set dataSource to nil for single page mode

4) don't allow cycles for 2-page mode

One can find full Swift realisation of a stable subclass at my detailed answer

The initial code and usage examples one can find at GitHub project.

malex
  • 9,874
  • 3
  • 56
  • 77