I've implemented a UIPageViewController in my iPad app. However, when the iPad is in portrait you can see all the pages, but when the iPad is in landscape you can’t see the last one, and if you are in the last page in portrait and change to landscape the app crashes with the following error:
Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘The >number of provided view controllers (1) doesn’t match the number required (2) for the >requested spine location (UIPageViewControllerSpineLocationMid)’
Because it needs 2 pages and there is only one.
What can I do when the “book” has an odd number of pages (7, for example) to avoid the previous exception?
The way I fixed it is like this.
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
DSBookletPageViewController *currentVC = (DSBookletPageViewController *)viewController;
NSUInteger currentIndex = [currentVC index];
if(currentIndex >= self.modelArray.count-1) {
if (currentIndex %2 == 0 && !isPortrait) {
//return an empty one
DSBookletPageViewController *newVC = [[DSBookletPageViewController alloc] init];
[newVC setIndex:currentIndex+1];
return newVC;
} else {
return nil;
}
}
currentIndex ++;
UIImage *currentPage = [self.modelArray objectAtIndex:currentIndex];
DSBookletPageViewController *newVC = [[DSBookletPageViewController alloc] initWithImage:currentPage andOrientation:isPortrait];
[newVC setIndex:currentIndex];
return newVC;
}
I basically check if I'm at the end of the array, or beyond it since I'm adding another page to the UIPageViewController outside of my Model array. If I'm there, and the current index is even and we're not in portrait orientation, then I add the page, if not, then I return nil.
I hope that helps.
Take a look at my answer here. Basically in your case you have to set the UIPageViewControllerDataSource which provides the content for all pages that are not shown at the time when the view appears for the first time.
Related
I am using pageViewController in my App. I have 9 pages which are created dymanically and each page have 1 question which have 4 multiple choice answers. Each option have custom UIButton with image and its "BlankedCheckBox" when the option is not selected and "MakredCheckBox" when its selected. Right now I can go to next page even if any of the available answer is not selected. I want to go to the next page only if user has selected one of the available option. The transition style of my pageViewController is scroll.
It seems to me like I have to add something in following method. Any help would be greatly appreciated, thanks.
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
NSUInteger index = ((PageContentViewController *) viewController).pageIndex;
if (index == NSNotFound) {
return nil;
}
index ++;
if (index == [self.questionTitles count]) {
return nil;
}
return [self viewControllerAtIndex:index];
}
You should not implement the dataSource methods. If you do the user can scroll using a gesture.
To achieve the behavior you want, set the page in code in the IBAction of the control:
- (IBAction)nextPage {
[self setViewControllers:[nextViewController]
direction: UIPageViewControllerNavigationDirectionForward
animated:YES
completion:nil];
}
I've been struggling all weekend with this problem and have spent a long time googling to find the solution without any success.
My use case is pretty simple and I can't believe how difficult it is to make such a trivial behaviour work correctly.
My app is a simple paginated flow where users swipe left or right to see the next or previous page. I have a UIPageViewController and each page contains a UITableView. I have had problems trying to keep track of the page index inside the viewControllerAfterViewController and viewControllerBeforeViewController functions for the reasons explained here: PageViewController delegate functions called twice
I've tried following all the suggested workarounds for this problem (keeping track of the index inside willTransitionToViewControllers and didFinishAnimating) but this doesn't solve my problem as the viewController*ViewController functions must still return a viewController and since they are initially called twice, the first returned viewController seems to be the one that gets used and the second pass through doesn't seem to have any affect.
Although I've seen many questions and blogs about this problem, I haven't seen a single example that shows how to consistently return the correct viewController from the viewController*ViewController functions and would be massively grateful for an example. The main issue I can't see a solution to is how to determine the next index inside willTransitionToViewControllers if I only have a single viewController whose content is dynamically updated on page load. It seems like a chicken and egg problem to me; I need to figure out what content to update the page with, but to do that I need to know what the index of the page is (which is part of the content).
Edit
Here is a distilled version of the affected code:
- (void)viewDidLoad {
[super viewDidLoad];
_pageViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"PageViewController"];
_pageViewController.dataSource = self;
_pageViewController.delegate = self;
[self.view addSubview:_pageViewController.view];
PageContentViewController *startingPage = [self viewControllerAtIndex:0];
NSArray *viewControllers = #[startingPage];
[_pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
// Don't do any index calculations as the result is inconsistent
return [self viewControllerAtIndex:nextIndex];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
// Don't do any index calculations as the result is inconsistent
return [self viewControllerAtIndex:nextIndex];
}
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers {
// This value never changes because the global _pageViewContentController hasn't been updated yet
nextIndex = [((PageContentViewController *)pendingViewControllers[0]) pageIndex];
}
- (PageContentViewController *)viewControllerAtIndex:(NSUInteger)index {
_pageContentViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"PageContentViewController"];
_pageContentViewController.pageIndex = index;
_pageContentViewController.title = index;
return _pageContentViewController;
}
I've debugged the order in which these functions get called and would expect the global _pageViewContentController to be populated with the correct data after the page has transitioned.
Edit 2
To give some more detail on my problem; each of my pages contains a title and a table of web links. Clicking on a link opens a WebViewController to display the selected web page. With Yunus' solution below, everything displays correctly (the title and links appear as expected) but the problem comes when clicking on a link as this loads a link from the next page. It seems like the rendering phase of the page happens at the point the data is correct, but it then reinitialises the page content controller with incorrect data after rendering has finished which is why the actual link data is wrong (even though the rendered data is good).
Let's keep all the related contentViewControllers in an array
self.contentViewControllers = [NSMutableArray new];
for(int i=0; i< MAX_INDEX; i++) {
[self.contentViewControllers addObject:[self viewControllerAtIndex:0]];
}
Then what we need to do is to decide which view controller we should show on after and before methods
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerBeforeViewController:(UIViewController *)viewController
{
for(int i=0;i<self.contentViewControllers.count ;i++)
{
if(viewController == [self.contentViewControllers objectAtIndex:i])
{
if(i-1 >= 0)
{
return [self.contentViewControllers objectAtIndex:i-1];
}
}
}
return nil;
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerAfterViewController:(UIViewController *)viewController
{
for(int i=0;i<self.contentViewControllers.count ;i++)
{
if(viewController == [self.contentViewControllers objectAtIndex:i])
{
if(i+1 < self.contentViewControllers.count)
{
return [self.contentViewControllers objectAtIndex:i+1];
}
}
}
return nil;
}
Then in the transition method
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers
{
UIViewController* viewController = [pendingViewControllers objectAtIndex:0];
NSInteger nextIndex = [self.contentViewControllers indexOfObject:viewController];
[self.pageControl setCurrentPage:nextIndex];
}
I have an UIScrollView that contains 3 UITableView and pagingEnabled = YES.
Users can pan to the left or right to switch between tables (just like the Notification Center of iOS).
I've handled almost all visual bugs (i can help if anyone needed), but the problem is every table have an UISearchBar. Which means in my controller, I've to create 3 UITableView, 3 UISearchBar and 3 UISearchDisplayController.
That will be one of a hell messy controller.
What the best practice in this case ?
It does sound tedious.
Why not create a single UIViewController having a SearchBar and a Tableview. Customize your viewController to be differentiated by tags, add some delegates probably and add it to your UIScrollview? That way, the coding will be structured and clean
I'm just spitballing though.
left or right table will move with your swipe. You have 1 class of your custom UIViewController but different instances.
- (SearchResultViewController *)viewControllerAtIndex:(NSUInteger)index
{
if (([[LeboncoinAgent shareAgent].searchConditions count] == 0) || (index >= [[LeboncoinAgent shareAgent].searchConditions count])) {
return nil;
}
// Create a new view controller and pass suitable data.
SearchResultViewController *pageContentViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"SearchResultViewControllerId"];
pageContentViewController.pageIndex = index;
pageContentViewController.controller = self;
return pageContentViewController;
}
#pragma mark - Page View Controller Data Source
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
NSUInteger index = ((SearchResultViewController*) viewController).pageIndex;
if ((index == 0) || (index == NSNotFound)) {
return nil;
}
index--;
_currentPageIndex = index;
return [self viewControllerAtIndex:index];
}
Before closing this question as a duplicate just see I'm facing a serious issue for the past two days.
I'm following UIPageViewController with storyboards tutorial.
I tried to develop my UIPageviewController. I've to show a number of user photos. I don't want to show apagecontrol in bottom so I created a custom page control instead. When I try to set the index to the UIPageControl it's not working properly - getting some delay some times reaching 5th page only its updating so the counts are not proper
This is my datasource:
-(UIViewController*)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
NSUInteger index = ((BMEnlargePhotoPageView*) viewController).pageIndex;
[self.enlargePhotoPageControl setCurrentPage:index];
if((index == 0 ) || (index==NSNotFound)){
return nil;
}
index--;
return [self viewControllerATindex:index];
}
-(UIViewController*)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
NSUInteger index = ((BMEnlargePhotoPageView*) viewController).pageIndex;
[self.enlargePhotoPageControl setCurrentPage:index];
if(index==NSNotFound){
return nil;
}
index++;
if(index==[self.userImages count])
{
return nil;
}
return [self viewControllerATindex:index];
}
- (BMEnlargePhotoPageView*)viewControllerATindex:(NSUInteger)index
{
if(([self.userImages count]==0) || (index >= [self.userImages count])){
}
BMEnlargePhotoPageView *PageContentViewControl = [self.storyboard instantiateViewControllerWithIdentifier:#"EnlargePhoto"];
PageContentViewControl.imageFIle =self.userImages[index];
PageContentViewControl.pageIndex = index;
return PageContentViewControl;
}
My other issue is that I want to find the current visible photos's index ID so that too I can't take properly - it's getting delayed value and is not correct.
Note: when I use animation page scroll everything works well but in scroll animation it's getting delayed but I want to show it as scroll only i tried many SO ansers nothing getting my problem some one guide or am i doing any wrong.
After long frustration week finally found which one good with iOS7 and don mind answering my question bcz no one answered so i supposed to fill the blank space.
In UIPageViewController there is delegate named didFinishAnimating: so in this there is a BOOL function which can identify your page turned completely or partially so using that BOOL you can check that and when you using even animation scroll it will be useful. Your code goes like this
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed{
if(completed){
COntentViewController *enlarge =[self.pageViewController.viewControllers lastObject];
self.enlargePhotoPageControl.currentPage = enlarge.pageIndex;
}
}
in this last object hold your last transmitted content if you partially scroll page the BOOL will not be get true it will indicate if the page scrolled completely this will help you those who wants to use custom page control. its working for me well how i expected set your pagecontrol index when the page scrolled completely unless the page indicator will not be set. It remains as last page count thx if i did any mistakes pls correct myself or even some good approach.
I have the following code to display a magazine type app. When the app is rotated it runs this code. I made sure that it is only run when rotated to supported orientations. When this function returns, the app fails with a SIGABRT. There is no other indication as to why.
I know it's this function because when I remove it the program does not fail.
- (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController
spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation
{
//If portrait mode, change to single page view
if(UIInterfaceOrientationIsPortrait(orientation)){
UIViewController *currentViewController = [self.pageViewController.viewControllers objectAtIndex:0];
NSArray *viewControllers = [NSArray arrayWithObject:currentViewController];
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];
self.pageViewController.doubleSided = NO;
return UIPageViewControllerSpineLocationMin;
//If landscape mode, change to double page view
}else{
//Get current view
UIViewController *currentViewController = [self.pageViewController.viewControllers objectAtIndex:0];
//Create an array to store, views
NSArray *viewControllers = nil;
NSUInteger currentIndex = self.currentPage;
//Conditional to check if needs page before or after
if(currentIndex == 1 || currentIndex %2 == 1){
UIViewController *nextViewController = [self pageViewController:self.pageViewController viewControllerAfterViewController:currentViewController];
viewControllers = [NSArray arrayWithObjects:currentViewController,nextViewController, nil];
}else{
UIViewController *previousViewController = [self pageViewController:self.pageViewController viewControllerBeforeViewController:currentViewController];
viewControllers = [NSArray arrayWithObjects:previousViewController, currentViewController, nil];
}
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];
return UIPageViewControllerSpineLocationMid;
}
//return UIPageViewControllerSpineLocationMid;
}
Alas, borrrden is probably right. One of your IBOutlets is probably missing from your XIB. Make sure ALL of your IBs are connected properly, and if the problem continues, say so.
Well you didn't provide the output from the console, which would be nice. Giving the code a quick look I would guess that one of your controllers (next or previous) is nil, and since you can't insert nil into an NSArray (except as the last object) it is throwing an error.
EDIT Well, my guess was wrong. The error message is saying that the UIViewControllers you are giving to it do not support the orientation that the page controller needs. This is because you have a method called shouldRotateToInterfaceOrientation: in your child UIViewControllers, and they are returning no for (in this case) left landscape.
I was getting the same error
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'All provided view controllers ((
"<ContentViewController: 0x6a7eac0>",
"<ContentViewController: 0x6d89f10>"
)) must support the pending interface orientation (UIInterfaceOrientationLandscapeLeft)'
Adding the following to my PageModel class, where the page layout is designed worked for me:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return YES;
}