UIPageViewController: return the current visible view

IphoneIosUipageviewcontroller

Iphone Problem Overview


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?

Iphone Solutions


Solution 1 - Iphone

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.

Then, you can get the currently presented View Controller by doing

self.viewControllers?.first

Solution 2 - Iphone

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.

Solution 3 - Iphone

Unfortunately, all above methods didn't help me. Nevertheless, I have found the solution by using tags. May be it's not the best, but it works and hope it helps someone:

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed 
{
    if (completed) {
        int currentIndex = ((UIViewController *)self.pageViewController.viewControllers.firstObject).view.tag;
        self.pageControl.currentPage = currentIndex;
    }
}

In Swift: (thanks to @Jessy)

func pageViewController(pageViewController: UIPageViewController,
    didFinishAnimating finished: Bool,
    previousViewControllers: [UIViewController],
    transitionCompleted completed: Bool)
{
    guard completed else { return }
    self.pageControl.currentPage = pageViewController.viewControllers!.first!.view.tag
}

Example: gist

Solution 4 - Iphone

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;
    
}

Solution 5 - Iphone

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;
}

Solution 6 - Iphone

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
	}
}

Solution 7 - Iphone

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.

Solution 8 - Iphone

I first used Corey's solution but it wasn't working on iOS5 then ended up using,

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

    if(completed) {
        _currentViewController = [pageViewController.viewControllers lastObject];
    }
}

It tried switching through different pages and it works well for now.

Solution 9 - Iphone

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.

Solution 10 - Iphone

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
      }

Solution 11 - Iphone

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
  }
}

Solution 12 - Iphone

Thank for your answer guys, i faced similar problem, had to store index. I slightly modify my code, paste it below:

- (MenuListViewController *)viewControllerAtIndex:(NSInteger)index {
    
    if (_menues.count < 1)
        return nil;
    
    //    MenuListViewController *childViewController = [MenuListViewController initWithSecondSetFakeItems];
    MenuListViewController *childViewController = self.menues[index];
    childViewController.index = index;
    
    return childViewController;
}

#pragma mark - Page View Controller Data Source

- (void)pageViewController:(UIPageViewController *)pageViewController
        didFinishAnimating:(BOOL)finished
   previousViewControllers:(NSArray<UIViewController *> *)previousViewControllers
       transitionCompleted:(BOOL)completed{
    
    if (completed) {
        
        NSUInteger currentIndex = ((MenuListViewController *)self.pageController.viewControllers.firstObject).index;
        NSLog(@"index %lu", (unsigned long)currentIndex);
    }
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
    NSUInteger index = [(MenuListViewController *)viewController index];
    
    if (index == 0)
        return nil;
    
    index --;
    
    return [self viewControllerAtIndex:index];
}


- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
    
    NSUInteger index = [(MenuListViewController *)viewController index];
    
    index ++;
    
    if (index == _menues.count)
        return nil;
    
    return [self viewControllerAtIndex:index];
}

Solution 13 - Iphone

How about asking for a viewController directly from the UIPageViewController (Swift 4 version):

fileprivate weak var currentlyPresentedVC: UIViewController?

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    currentlyPresentedVC = pageViewController.viewControllers?.first
}

Or, if you just need the currently presented view controller at some point of time, simply use pageViewController.viewControllers?.first at that time.

Solution 14 - Iphone

- (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

Solution 15 - Iphone

UIViewController *viewController = [pageViewController.viewControllers objectAtIndex:0];
NSUInteger currentIndex = [(ViewController*) viewController indexNumber];

It will return current page index. and must use this code under the delegate function of UIPageViewController (didFinishAnimating).

Solution 16 - Iphone

Below demo code (in Swift 2) that demonstrates how this is done by implementing a simple image swiper tutorial. Comments in the code itself :

import UIKit

/*
VCTutorialImagePage represents one page show inside the UIPageViewController.
You should create this page in your interfacebuilder file:
- create a new view controller
- set its class to VCTutorialImagePage
- sets its storyboard identifier to "VCTutorialImagePage" (needed for the loadView function)
- put an imageView on it and set the contraints (I guess to top/bottom/left/right all to zero from the superview)
- connect it to the "imageView" outlet
*/

class VCTutorialImagePage : UIViewController {
    //image to display, configure this in interface builder
    @IBOutlet weak var imageView: UIImageView!
    //index of this page
    var pageIndex : Int = 0
    
    //loads a new view via the storyboard identifier
    static func loadView(pageIndex : Int, image : UIImage) -> VCTutorialImagePage {
        let storyboard = UIStoryboard(name: storyBoardHome, bundle: nil)
        let vc = storyboard.instantiateViewControllerWithIdentifier("VCTutorialImagePage") as! VCTutorialImagePage
        vc.imageView.image      = image
        vc.pageIndex            = pageIndex
        return vc
    }
}


/*
VCTutorialImageSwiper takes an array of images (= its model) and displays a UIPageViewController 
where each page is a VCTutorialImagePage that displays an image. It lets you swipe throught the 
images and will do a round-robbin : when you swipe past the last image it will jump back to the 
first one (and the other way arround).

In this process, it keeps track of the current displayed page index
*/

class VCTutorialImageSwiper: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
    
    //our model = images we are showing
    let tutorialImages : [UIImage] = [UIImage(named: "image1")!, UIImage(named: "image2")!,UIImage(named: "image3")!,UIImage(named: "image4")!]
    //page currently being viewed
    private var currentPageIndex : Int = 0 {
        didSet {
            currentPageIndex=cap(currentPageIndex)
        }
    }
    //next page index, temp var for keeping track of the current page
    private var nextPageIndex : Int = 0
    
    
    //Mark: - life cylce
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //setup page vc
        dataSource=self
        delegate=self
        setViewControllers([pageForindex(0)!], direction: .Forward, animated: false, completion: nil)
    }
    
    
    //Mark: - helper functions
    
    func cap(pageIndex : Int) -> Int{
        if pageIndex > (tutorialImages.count - 1) {
            return 0
        }
        if pageIndex < 0 {
            return (tutorialImages.count - 1)
        }
        return pageIndex
    }
    
    func carrouselJump() {
        currentPageIndex++
        setViewControllers([self.pageForindex(currentPageIndex)!], direction: .Forward, animated: true, completion: nil)
    }
    
    func pageForindex(pageIndex : Int) -> UIViewController? {
        guard (pageIndex < tutorialImages.count) && (pageIndex>=0) else { return nil }
        return VCTutorialImagePage.loadView(pageIndex, image: tutorialImages[pageIndex])
    }
    
    func indexForPage(vc : UIViewController) -> Int {
        guard let vc = vc as? VCTutorialImagePage else {
            preconditionFailure("VCPagImageSlidesTutorial page is not a VCTutorialImagePage")
        }
        return vc.pageIndex
    }
    
    
    //Mark: - UIPageView delegate/datasource
    
    
    func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
        return pageForindex(cap(indexForPage(viewController)+1))
    }
    
    func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
        return pageForindex(cap(indexForPage(viewController)-1))
    }
    
    func pageViewController(pageViewController: UIPageViewController, willTransitionToViewControllers pendingViewControllers: [UIViewController]) {
        nextPageIndex = indexForPage(pendingViewControllers.first!)
    }
    
    func pageViewController(pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        if !finished { return }
        currentPageIndex = nextPageIndex
    }
    
    func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
        return tutorialImages.count
    }
    
    func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int {
        return currentPageIndex
    }
    
}

Solution 17 - Iphone

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.

Solution 18 - Iphone

This is the solution I came up with:

class DefaultUIPageViewControllerDelegate: NSObject, UIPageViewControllerDelegate {

    // MARK: Public values
    var didTransitionToViewControllerCallback: ((UIViewController) -> Void)?

    // MARK: Private values
    private var viewControllerToTransitionTo: UIViewController!

    // MARK: Methods
    func pageViewController(
        _ pageViewController: UIPageViewController,
        willTransitionTo pendingViewControllers: [UIViewController]
    ) {
        viewControllerToTransitionTo = pendingViewControllers.last!
    }

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

Usage:

 let pageViewController = UIPageViewController()
 let delegate = DefaultUIPageViewControllerDelegate()

 delegate.didTransitionToViewControllerCallback = {
    pageViewController.title = $0.title
 }

 pageViewController.title = viewControllers.first?.title
 pageViewController.delegate = delegate

Make sure to set the initial title

Solution 19 - Iphone

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
    }
}

Solution 20 - Iphone

In swift 5 and following sirvine answer

extension InnerDetailViewController: UIPageViewControllerDelegate {

    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    
        if completed {
            guard let newIndex = embeddedViewControllers.firstIndex(where: { $0 == pageViewController.viewControllers?.last }) else { return }
            print(newIndex)
            currentEmbeddedViewControllerIndex = newIndex
        }

    }

} 

In this case i don't care what class of UIViewController are embedded

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJanView Question on Stackoverflow
Solution 1 - IphoneOle BegemannView Answer on Stackoverflow
Solution 2 - IphoneEddy BorjaView Answer on Stackoverflow
Solution 3 - IphoneVictorView Answer on Stackoverflow
Solution 4 - IphoneCorey FloydView Answer on Stackoverflow
Solution 5 - IphonesirvineView Answer on Stackoverflow
Solution 6 - IphoneNikola MilicevicView Answer on Stackoverflow
Solution 7 - IphoneWahib Ul HaqView Answer on Stackoverflow
Solution 8 - IphoneBasit AliView Answer on Stackoverflow
Solution 9 - Iphone0yeojView Answer on Stackoverflow
Solution 10 - Iphonelguerra10View Answer on Stackoverflow
Solution 11 - IphoneYariv NissimView Answer on Stackoverflow
Solution 12 - IphoneEvgeniy KlebanView Answer on Stackoverflow
Solution 13 - IphoneMilan NosáľView Answer on Stackoverflow
Solution 14 - IphoneAvinash KashyapView Answer on Stackoverflow
Solution 15 - IphoneVinay PodiliView Answer on Stackoverflow
Solution 16 - IphoneHixFieldView Answer on Stackoverflow
Solution 17 - IphonejaroraView Answer on Stackoverflow
Solution 18 - IphonePeter CombeeView Answer on Stackoverflow
Solution 19 - Iphoneswift taylorView Answer on Stackoverflow
Solution 20 - IphoneReimond HillView Answer on Stackoverflow