How to put the UIPageControl element on top of the sliding pages within a UIPageViewController?
Asked Answered
F

7

61

Regarding to this tutorial by AppCoda about how to implement a app with UIPageViewController I'd like to use a custom page control element on top of the pages instead of at the bottom.

When I just put a page control element on top of the single views which will be displayed, the logical result is that the control elements scrolls with the page view away from the screen.

How is it possible to put the control element on top of the views so the page views are full screen (like with an image) so the user can see the views underneath the fixed control element?

I attached an example screenshot - credits to AppCoda and Path:

enter image description here

Fulfillment answered 10/1, 2014 at 13:30 Comment(0)
F
51

After further investigation and searching I found a solution, also on stackoverflow.

The key is the following message to send to a custom UIPageControl element:

[self.view bringSubviewToFront:self.pageControl];

The AppCoda tutorial is the foundation for this solution:

Add a UIPageControl element on top of the RootViewController - the view controller with the arrow.

Create a related IBOutlet element in your ViewController.m.

In the viewDidLoad method you should then add the following code as the last method you call after adding all subviews.

[self.view bringSubviewToFront:self.pageControl];

To assign the current page based on the pageIndex of the current content view you can add the following to the UIPageViewControllerDataSource methods:

- (UIPageViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
    // ...
    index--;
    [self.pageControl setCurrentPage:index];

    return [self viewControllerAtIndex:index];
}

- (UIPageViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
    // ...
    index++;
    [self.pageControl setCurrentPage:index];

    // ...
    return [self viewControllerAtIndex:index];
}
Fulfillment answered 10/1, 2014 at 15:50 Comment(5)
strangely for me, setting the pageControl page before incrementing/decrementing the index worked. The viewController that I was getting from the method was that of the new view controller and not the one before transition.Hasseman
That's not strange at all; it's expected. Those methods are called on the current view controller to determine what comes up next.Eleanoraeleanore
@Fulfillment How to create a related IBOutlet element.Parch
@elavarasan which element? the self.pageControl is for example an IBOutlet to the Page Control element on the view...Fulfillment
Handling pageControl in the datasource methods shouldn't really be recommended. As a datasouce, the only thing you should do is return the data requested (and avoid other logic) - you have no control on when these methods will be called - that's up to the implementation. For example, after a scroll you may observe the PageViewController 'prefetching' the next neighbouring page ahead of time. This totally messes with any checky logic you might sneak in (and probably explains why @Hasseman gets a different result to @sn3ek). (Better: use the delegate instead)Gilbertegilbertian
L
55

I didn't have the rep to comment on the answer that originated this, but I really like it. I improved the code and converted it to swift for the below subclass of UIPageViewController:

class UIPageViewControllerWithOverlayIndicator: UIPageViewController {
    override func viewDidLayoutSubviews() {
        for subView in self.view.subviews as! [UIView] {
            if subView is UIScrollView {
                subView.frame = self.view.bounds
            } else if subView is UIPageControl {
                self.view.bringSubviewToFront(subView)
            }
        }
        super.viewDidLayoutSubviews()
    }
}

Clean and it works well. No need to maintain anything, just make your page view controller an instance of this class in storyboard, or make your custom page view controller class inherit from this class instead.

Lick answered 30/1, 2015 at 18:19 Comment(3)
when i put this for loop in ViewDidLoad I can find the scrollView but not the UIPageControl view. am I missing something ?Kino
@Kino perhaps the controller is missing the UIPageViewController type? It's hard to say without an example of the exact situation.Lick
I'm gonna bookmark this just for how versatile and useful this is. Thankx so much!!Lubricator
F
51

After further investigation and searching I found a solution, also on stackoverflow.

The key is the following message to send to a custom UIPageControl element:

[self.view bringSubviewToFront:self.pageControl];

The AppCoda tutorial is the foundation for this solution:

Add a UIPageControl element on top of the RootViewController - the view controller with the arrow.

Create a related IBOutlet element in your ViewController.m.

In the viewDidLoad method you should then add the following code as the last method you call after adding all subviews.

[self.view bringSubviewToFront:self.pageControl];

To assign the current page based on the pageIndex of the current content view you can add the following to the UIPageViewControllerDataSource methods:

- (UIPageViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
    // ...
    index--;
    [self.pageControl setCurrentPage:index];

    return [self viewControllerAtIndex:index];
}

- (UIPageViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
    // ...
    index++;
    [self.pageControl setCurrentPage:index];

    // ...
    return [self viewControllerAtIndex:index];
}
Fulfillment answered 10/1, 2014 at 15:50 Comment(5)
strangely for me, setting the pageControl page before incrementing/decrementing the index worked. The viewController that I was getting from the method was that of the new view controller and not the one before transition.Hasseman
That's not strange at all; it's expected. Those methods are called on the current view controller to determine what comes up next.Eleanoraeleanore
@Fulfillment How to create a related IBOutlet element.Parch
@elavarasan which element? the self.pageControl is for example an IBOutlet to the Page Control element on the view...Fulfillment
Handling pageControl in the datasource methods shouldn't really be recommended. As a datasouce, the only thing you should do is return the data requested (and avoid other logic) - you have no control on when these methods will be called - that's up to the implementation. For example, after a scroll you may observe the PageViewController 'prefetching' the next neighbouring page ahead of time. This totally messes with any checky logic you might sneak in (and probably explains why @Hasseman gets a different result to @sn3ek). (Better: use the delegate instead)Gilbertegilbertian
P
27

sn3ek Your answer got me most of the way there. I didn't set the current page using the viewControllerCreation methods though.

I made my ViewController also the delegate of the UIPageViewController. Then I set the PageControl's CurrentPage in that method. Using the pageIndex maintained I'm the ContentViewController mention in the original article.

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
  APPChildViewController *currentViewController = pageViewController.viewControllers[0];
  [self.pageControl setCurrentPage:currentViewController.pageIndex];
}

don't forget to add this to viewDidLoad

self.pageViewController.delegate = self;

To follow up on PropellerHead's comment the interface for the ViewController will have the form

@interface ViewController : UIViewController <UIPageViewControllerDataSource, UIPageViewControllerDelegate>
Polymer answered 15/4, 2014 at 0:26 Comment(1)
Just to be clear. The delegate of UIPageViewController is UIPageViewControllerDelegate, so the interface of your ViewController will look something like this: @interface ViewController : UIViewController <UIPageViewControllerDataSource, UIPageViewControllerDelegate>Clinker
S
3

The same effect can be achieved simply by subclassing UIPageViewController and overriding viewDidLayoutSubviews as follows:

-(void)viewDidLayoutSubviews {
    UIView* v = self.view;
    NSArray* subviews = v.subviews;
    if( [subviews count] == 2 ) {
        UIScrollView* sv = nil;
        UIPageControl* pc = nil;
        for( UIView* t in subviews ) {
            if( [t isKindOfClass:[UIScrollView class]] ) {
                sv = (UIScrollView*)t;
            } else if( [t isKindOfClass:[UIPageControl class]] ) {
                pc = (UIPageControl*)t;
            }
        }
        if( sv != nil && pc != nil ) {
            // expand scroll view to fit entire view
            sv.frame = v.bounds;
            // put page control in front
            [v bringSubviewToFront:pc];
        }
    }
    [super viewDidLayoutSubviews];
}

Then there is no need to maintain a seperate UIPageControl and such.

Splendor answered 20/7, 2014 at 15:23 Comment(1)
I actually liked this answer. It worked well. Until I put on an iPhone 4S running iOS 7.1. At which point it went infinite. Not sure why, but beware.Milwaukee
L
2

You have to implement a custom UIPageControl and add it to the view. As others have mentioned, view.bringSubviewToFront(pageControl) must be called.

I have an example of a view controller with all the code on setting up a custom UIPageControl (in storyboard) with UIPageViewController

There are 2 methods which you need to implement to set the current page indicator.

func pageViewController(pageViewController: UIPageViewController, willTransitionToViewControllers pendingViewControllers: [UIViewController]) {
    pendingIndex = pages.indexOf(pendingViewControllers.first!)
}

func pageViewController(pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    if completed {
        currentIndex = pendingIndex
        if let index = currentIndex {
            pageControl.currentPage = index
        }
    }
}
Lenitalenitive answered 8/3, 2016 at 8:33 Comment(0)
M
1

Here is a RxSwift/RxCocoa answer I put together after looking at the other replies.

let pages = Variable<[UIViewController]>([])
let currentPageIndex = Variable<Int>(0)
let pendingPageIndex = Variable<(Int, Bool)>(0, false)

let db = DisposeBag()

override func viewDidLoad() {
    super.viewDidLoad()
    pendingPageIndex
        .asDriver()
        .filter { $0.1 }
        .map { $0.0 }
        .drive(currentPageIndex)
        .addDisposableTo(db)

    currentPageIndex.asDriver()
        .drive(pageControl.rx.currentPage)
        .addDisposableTo(db)
}

func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
    if let index = pages.value.index(of: pendingViewControllers.first!) {
        pendingPageIndex.value = (index, false)
    }
}

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    pendingPageIndex.value = (pendingPageIndex.value.0, completed)
}
Mesentery answered 31/12, 2016 at 4:35 Comment(0)
T
0

Here's the Swifty 2 answer very much based on @zerotool's answer above. Just subclass UIPageViewController and then add this override to find the scrollview and resize it. Then grab the page control and move it to the top of everything else. You also need to set the page controls background color to clear. Those last two lines could go in your app delegate.

override func viewDidLayoutSubviews() {

    super.viewDidLayoutSubviews()

    var sv:UIScrollView?
    var pc:UIPageControl?

    for v in self.view.subviews{

        if v.isKindOfClass(UIScrollView) {

            sv = v as? UIScrollView

        }else if v.isKindOfClass(UIPageControl) {

            pc = v as? UIPageControl
        }
    }

    if let newSv = sv {

        newSv.frame = self.view.bounds
    }

    if let newPc = pc {

        self.view.bringSubviewToFront(newPc)
    }
}

let pageControlAppearance = UIPageControl.appearance()
pageControlAppearance.backgroundColor = UIColor.clearColor()

btw - I'm not noticing any infinite loops, as mentioned above.

Touchandgo answered 25/2, 2016 at 13:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.