UIPageViewController: 返回当前可见视图

如何知道 UIPageViewController中显示的当前页面/视图是什么?

我已经重写了子视图的 viewDidAppear方法,以便它们在其 viewDidAppear方法中向父视图发送一个 id。

然而,问题在于: 我不能可靠地使用这个 id 作为显示页面的 id。因为如果用户翻页,但中途决定停止翻页并将页面放回,则 viewDidAppear将已被调用。(在卷曲的页面后面可以看到视图)。

也许只有当前视图消失时,我才应该切换到一个新的 id。但是我想知道是否有一种更简单的方法来返回当前可见的视图?

87666 次浏览

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

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.

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

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.

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

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

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

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

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.

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
}

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
}


}

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

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

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.

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

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

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.

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