6

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).

Community
  • 1
  • 1
bobby
  • 61
  • 3
  • Can you share some related parts of your code? – Yunus Eren Güzel Aug 16 '15 at 10:49
  • @YunusErenGuzel - code snippets added. – bobby Aug 16 '15 at 11:12
  • How do you open this WebViewController? Inside the UIPageViewController or as modal or etc? – Yunus Eren Güzel Aug 16 '15 at 12:30
  • 1
    Wow - it's amazing how describing your problem causes you to look at it in a completely new light! I realised that I was initialising my web links in the viewDidLoad function of my page content view controller. This was causing the rendered data to be different to the actual data inside the controller. I simply removed that initialisation and now everything is working as expected. Thanks for bouncing ideas around Yunus - it helped me approach the problem in a different way. – bobby Aug 16 '15 at 12:43
  • I am happy that you solved your problem ;) – Yunus Eren Güzel Aug 17 '15 at 06:47

1 Answers1

3

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];
}
Yunus Eren Güzel
  • 3,018
  • 11
  • 36
  • 63
  • Hi Yunus. Thanks for your help. I have modified my code to try your suggestion (this is something I had already tried over the weekend but without success). In your example, you're setting the nextIndex value but it is never used anywhere. Am I missing something here? – bobby Aug 16 '15 at 11:44
  • Actually I keep that empty for you to however you would like to use. I use it with SMPageControl to keep the index with `[self.pageControl setCurrentPage:nextIndex];` – Yunus Eren Güzel Aug 16 '15 at 11:47
  • You can use this ViewController to see how it works. https://github.com/yunuserenguzel/sonicraph-ios/blob/master/sonicraph/View%20Controllers/SNCGoThroughViewController.m – Yunus Eren Güzel Aug 16 '15 at 11:49
  • Your code gives me the same problem. When I debug, this is the order of events: 1. App loads 2. Swipe to next page 3. viewControllerAfterViewController is invoked 4. viewControllerBeforeViewController is invoked 5. willTransitionToViewControllers is invoked 6. viewControllerAfterViewController is invoked 7. Next page is displayed but with incorrect data due to the extra calls to viewController*ViewController – bobby Aug 16 '15 at 11:50
  • Yes, in my application the same calls are happening. When I try to swipe to the next view controller, after, before and after is called. I am not sure why. However, you should not be affected by this if you use my solution. – Yunus Eren Güzel Aug 16 '15 at 12:03
  • I've added more detail to the original post. – bobby Aug 16 '15 at 12:19
  • 1
    This is not good solution, because you store all viewControllers instances. UIPageViewController should manage this in delegate methods. If you had a lot of pages this approach is very inefficient. – Deny Sep 19 '15 at 21:49