iphone - with - UIPageViewController: return the current visible view




uipageviewcontroller with buttons (12)

How do you know what is the current page/view displayed inside an UIPageViewController?

I have overridden the viewDidAppear method of my child views, so that they send an id to the parent view in their viewDidAppear method.

However, the problem is this: i cannot reliably use that id as id for the displayed page. because if the user turns the page but halfway through decides to stop the turning and put the page back, viewDidAppear will already have been called. (the view is visible behind the curled page).

Maybe i should only switch to a new id if the current view disappears. But I wonder if there is not a more simple way to return the view that is currently visible?


Swift 4

No unnecessary code. 3 ways of doing it. Using UIPageViewControllerDelegate method.

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    guard completed else { return }

    // using content viewcontroller's index
    guard let index = (pageViewController.viewControllers?.first as? ContentViewController)?.index else { return }

    // using viewcontroller's view tag
    guard let index = pageViewController.viewControllers?.first?.view.tag else { return }

    // switch on viewcontroller
    guard let vc = pageViewController.viewControllers?.first else { return }
    let index: Int
    switch vc {
    case is FirstViewController:
        index = 0
    case is SecondViewController:
        index = 1
    default:
        index = 2
    }
}

As of iOS 6 I've found that the viewControllers property of UIPageViewController constantly updates so that it will always hold the one view controller that represents the current page, and nothing else. Thus, you can access the current page by calling viewControllers[0] (Assuming you only show one view controller at a time).

The viewController array only updates once the page "locks" into place, so if a user decides to partially reveal the next page it doesn't become the "current" page unless they complete the transition.

If you want to keep track of the "page numbers" assign your view controllers an index value as you create them through the UIPageViewController datasource methods.


So for example:

-(void)autoAdvance
    {
    UIViewController *currentVC = self.viewControllers[0];
    NSUInteger currentIndex = [myViewControllers indexOfObject:currentVC];

    if ( currentIndex >= (myViewControllers.count-1) ) return;

    [self setViewControllers:@[myViewControllers[ currentIndex+1 ]]
        direction:UIPageViewControllerNavigationDirectionForward
        animated:YES
        completion:nil];
    }
-(NSInteger)presentationIndexForPageViewController:
                         (UIPageViewController *)pageViewController
    {
    // return 0;
    UIViewController *currentVC = self.viewControllers[0];
    NSUInteger currentIndex = [myViewControllers indexOfObject:currentVC];
    return currentIndex;
    }

But note the comments that this is unreliable.


Building on Ole's Answer…

This is how I implemented the 4 methods to track the current page and update the page indicator to the correct index:

- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController{

    return (NSInteger)[self.model count];

}

- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController{

    return (NSInteger)self.currentIndex;
}

- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers{

    SJJeanViewController* controller = [pendingViewControllers firstObject];
    self.nextIndex = [self indexOfViewController:controller];

}

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed{

    if(completed){

        self.currentIndex = self.nextIndex;

    }

    self.nextIndex = 0;

}

I am keeping track of the page index by using a small function and specifying pageIndex as static NSInteger.

-(void) setPageIndex
{
    DataViewController *theCurrentViewController = [self.pageViewController.viewControllers objectAtIndex:0];

    pageIndex = [self.modelController indexOfViewController:theCurrentViewController];
}

and calling [self setPageIndex]; inside the function specified by Ole and also after detecting the change in orientation.


I have a viewControllers array, that I display in the UIPageViewController.

extension MyViewController: UIPageViewControllerDataSource {

func presentationCount(for pageViewController: UIPageViewController) -> Int {
    return self.viewControllers.count
}

func presentationIndex(for pageViewController: UIPageViewController) -> Int {
    return self.currentPageIndex
}
}




extension MyViewController: UIPageViewControllerDelegate {

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {

    if !completed { return }

    guard let viewController = previousViewControllers.last, let index = indexOf(viewController: viewController) else {
        return
    }

    self.currentPageIndex = index

}

fileprivate func indexOf(viewController: UIViewController) -> Int? {
    let index = self.viewControllers.index(of: viewController)
    return index
}
}

Important thing to note here is that the setViewControllers method of UIPageViewController does not give any delegate callback. The delegate callbacks only represent user touch actions in the UIPageViewController.


I've been using view.tag for a while now, trying to keep track of the current page was too complicated.

In this code the index is stored within the tag property of each view and is used to fetch the next or previous VC. Using this method it's also possible to create an infinite scroll. Check out the comment in code to view this solution as well:

extension MyPageViewController: UIPageViewControllerDataSource {

  func viewControllerWithIndex(var index: Int) -> UIViewController! {
    let myViewController = storyboard?.instantiateViewControllerWithIdentifier("MyViewController") as MyViewController

    if let endIndex = records?.endIndex {
      if index < 0 || index >= endIndex { return nil }
      // Instead, We can normalize the index to be cyclical to create infinite scrolling
      // if index < 0 { index += endIndex }
      // index %= endIndex
    }

    myViewController.view.tag = index
    myViewController.record = records?[index]

    return myViewController
  }

  func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
    let index = viewController.view?.tag ?? 0
    return viewControllerWithIndex(index + 1)
  }

  func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
    let index = viewController.view?.tag ?? 0
    return viewControllerWithIndex(index - 1)
  }

  func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
    return records?.count ?? 0
  }

  func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int {
    return (pageViewController.viewControllers.first as? UIViewController)?.view.tag ?? 0
  }
}

The simplest way to approach this IMHO is to use the PageControl to store the potential outcome of the transition and then revert if the transition was cancelled. This means that the page control changes as soon as the user starts swiping, which is ok by me. This requires that you have your own array of UIViewControllers (in this example called allViewControllers)

func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
    if let index = self.allViewControllers.index(of: pendingViewControllers[0]) {
        self.pageControl.currentPage = index
    }
}

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    if !completed, let previousIndex = self.allViewControllers.index(of: previousViewControllers[0]) {
        self.pageControl.currentPage = previousIndex
    }
}

The solution below worked for me.

Apple could avoid a lot of hassle by making the native UIPageViewController scroll view pagination more configurable. I had to resort to overlaying a new UIView and UIPageControl just because the native UIPageViewController pagination won't support a transparent background or repositioning within the view frame.

- (void)pageViewController:(UIPageViewController *)pvc didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
  if (!completed)
  {
    return;
  }
  NSUInteger currentIndex = [[self.pageViewController.viewControllers lastObject] index];
  self.pageControl.currentPage = currentIndex;
}

This works for me reliably

I have a custom UIPageController. This pageController.currentPage is updated from the displayed UIViewController in the viewWillAppear

   var delegate: PageViewControllerUpdateCurrentPageNumberDelegate?

      init(delegate: PageViewControllerUpdateCurrentPageNumberDelegate ){
        self.delegate = delegate
        super.init(nibName: nil, bundle: nil)
      }

      required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }

    override func viewWillAppear(animated: Bool) {

        if delegate != nil {
          self.delegate!.upateCurrentPageNumber(0) //(0) is the pageNumber corresponding to the displayed controller
        }
      } 

    //In the pageViewController 

    protocol PageViewControllerUpdateCurrentPageNumberDelegate {

      func upateCurrentPageNumber(currentPageIndex: Int)
    }

     create the view display controllers initializing with the delegate

    orderedViewControllers = {
              return [
                IntroductionFirstPageViewController(delegate: self),
                IntroductionSecondPageViewController(delegate: self),
                IntroductionThirdPageViewController(delegate: self)
              ]

            }()

    the function implementing the protocol

    func upateCurrentPageNumber(currentPageIndex: Int){
        pageControl.currentPage = currentPageIndex
      }

Unfortunately nothing above works for me.

I have two view controllers and when I slightly (around 20px) scroll the last view backwards it triggers the delegate:

pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:

and saying that the current page (index) is 0 which is wrong.

Using delegate inside child viewController something like:

- (void)ViewController:(id)VC didShowWithIndex:(long)page;

// and a property

@property (nonatomic) NSInteger index;

that is triggered inside viewDidAppear like:

- (void)viewDidAppear:(BOOL)animated
{
    ...

    [self.delegate ViewController:self didShowWithIndex:self.index];
}

Worked for me.


You should manually keep track of the current page. The delegate method pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted: will tell you when to update that variable. The last argument of the method transitionCompleted: can tell you whether a user completed a page turn transition or not.


- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed {

    NSLog(@"Current Page = %@", pageViewController.viewControllers);

    UIViewController *currentView = [pageViewController.viewControllers objectAtIndex:0];

    if ([currentView isKindOfClass:[FirstPageViewController class]]) {
             NSLog(@"First View");
        }
        else if([currentView isKindOfClass:[SecondPageViewController class]]) {
             NSLog(@"Second View");
        }
        else if([currentView isKindOfClass:[ThirdViewController class]]) {
              NSLog(@"Third View");
        }
}

//pageViewController.viewControllers always return current visible View ViewController




uipageviewcontroller