19

I have a simple UIPageViewController which displays the default UIPageControl at the bottom of the pages. I wonder if it's possible to modify the position of the UIPageControl, e.g. to be on top of the screen instead of the bottom. I've been looking around and only found old discussions that say I need to create my own UIPageControl. Is this thing simpler with iOS8 and 9? Thanks.

Nguyen Thu
  • 193
  • 1
  • 1
  • 4

14 Answers14

40

Yes, you can add custom page controller for that.

self.pageControl = [[UIPageControl alloc] initWithFrame:CGRectMake(0, self.view.frame.size.height - 50, self.view.frame.size.width, 50)]; // your position

[self.view addSubview: self.pageControl];

then remove

- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController

and

- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController

Then add another delegate method:

- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray<UIViewController *> *)pendingViewControllers
 {
     PageContentViewController *pageContentView = (PageContentViewController*) pendingViewControllers[0];
     self.pageControl.currentPage = pageContentView.pageIndex;
 }
pkamb
  • 33,281
  • 23
  • 160
  • 191
Jan
  • 1,744
  • 3
  • 23
  • 38
  • 1
    Thanks for the answer. If I understand well, the part of removing presentationCountForPageViewController: and presentationIndexForPageViewController: is to hide the default page controller, is it right? – Nguyen Thu Sep 29 '15 at 08:17
  • 5
    This is well described, but doesn't work completely. As Jiri points out, this breaks if the user swipes part way, but cancels (since it updates the page control as if the page transition actually happened). The solution I went with was to update the page control in delegate didFinishAnimating. This behavior matches Apple's behavior like on the iOS Home Screen (updates page control after transition is complete) – davew Apr 15 '16 at 17:36
  • I am trying to follow the way described in answer but the dots are not appearing at all. Anywhere I am going wrong? – Rohan Sanap Apr 28 '16 at 07:36
  • @RohanSanap you should set the numberOfPages property of pageControl – Mohan Meruva Aug 14 '20 at 17:21
10

Just lookup PageControl in PageViewController subclass and set frame, location or whatever you want

override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        for subView in view.subviews {
            if  subView is  UIPageControl {
                subView.frame.origin.y = self.view.frame.size.height - 164
            }
        }
    }
Trung Phan
  • 923
  • 10
  • 18
  • This solution worked best and with the least amount of disruption to my existing code setup. – Karthik Kannan Feb 26 '18 at 19:17
  • 1) This could break if Apple changes its implementation detail, 2) the bottom of the `UIPageViewController` still shows empty space, even if it doesn't contain the dots. – Zorayr Aug 10 '20 at 21:38
9

Override the viewDidLayoutSubviews() of the pageviewcontroller and use this

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    // get pageControl and scroll view from view's subviews
    let pageControl = view.subviews.filter{ $0 is UIPageControl }.first! as! UIPageControl
    let scrollView = view.subviews.filter{ $0 is UIScrollView }.first! as! UIScrollView
    // remove all constraint from view that are tied to pagecontrol
    let const = view.constraints.filter { $0.firstItem as? NSObject == pageControl || $0.secondItem as? NSObject == pageControl }
    view.removeConstraints(const)

    // customize pagecontroll
    pageControl.translatesAutoresizingMaskIntoConstraints = false
    pageControl.addConstraint(pageControl.heightAnchor.constraintEqualToConstant(35))
    pageControl.backgroundColor = view.backgroundColor

    // create constraints for pagecontrol
    let leading = pageControl.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor)
    let trailing = pageControl.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor)
    let bottom = pageControl.bottomAnchor.constraintEqualToAnchor(scrollView.topAnchor, constant:8) // add to scrollview not view

    // pagecontrol constraint to view
    view.addConstraints([leading, trailing, bottom])
    view.bounds.origin.y -= pageControl.bounds.maxY
}
Cjay
  • 1,093
  • 6
  • 11
  • 2
    This gives a really good control over everything and prevent the addition of a new pageControl. Thanks! – chrilith Apr 25 '16 at 16:51
7

The Shameerjan answer is very good, but it needs one more thing to work properly, and that is implementation of another delegate method:

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

    // If user bailed our early from the gesture, 
    // we have to revert page control to previous position
    if !completed {
         let pageContentView = previousViewControllers[0] as! PageContentViewController;
         self.pageControl.currentPage = pageContentView.pageIndex;
    }
}

This is because if you don't, if you move the page control just so slightly, it will go back to previous position - but the page control will show different page.

Hope it helps!

Jiri Trecak
  • 5,092
  • 26
  • 37
  • It's a good comment, but you have a mixture of Swift and Objective-C code. Maybe.. let pageContentView = previousViewControllers.first – EPage_Ed Apr 06 '16 at 19:09
7

Swift 4 version of @Jan's answer with fixed bug when the user cancels transition:

First, you create custom pageControl:

let pageControl = UIPageControl()

You add it to the view and position it as you want:

self.view.addSubview(self.pageControl)
self.pageControl.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    self.pageControl.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor, constant: 43),
    self.pageControl.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -33)
])

Then you need to initialize the pageControl:

self.pageControl.numberOfPages = self.dataSource.controllers.count

Finally, you need to implement UIPageViewControllerDelegate, and its method pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:):

func pageViewController(_ pageViewController: UIPageViewController,
                        didFinishAnimating finished: Bool,
                        previousViewControllers: [UIViewController],
                        transitionCompleted completed: Bool) {
    // this will get you currently presented view controller
    guard let selectedVC = pageViewController.viewControllers?.first else { return }

    // and its index in the dataSource's controllers (I'm using force unwrap, since in my case pageViewController contains only view controllers from my dataSource)
    let selectedIndex = self.dataSource.controllers.index(of: selectedVC)!
    // and we update the current page in pageControl
    self.pageControl.currentPage = selectedIndex
}

Now in comparison with @Jan's answer, we update self.pageControl.currentPage using pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:) (shown above), instead of pageViewController(_:willTransitionTo:). This overcomes the problem of cancelled transition - pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:) is called always when a transition was completed (it also better mimics the behavior of standard page control).

Finally, to remove the standard page control, be sure to remove implementation of presentationCount(for:) and presentationIndex(for:) methods of the UIPageViewControllerDataSource - if the methods are implemented, the standard page control will be presented.

So, you do NOT want to have this in your code:

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


func presentationIndex(for pageViewController: UIPageViewController) -> Int {
    return 0
}
Milan Nosáľ
  • 19,169
  • 4
  • 55
  • 90
4

For swift:

self.pageController = UIPageControl(
                                     frame: CGRect(
                                         x: 0,
                                         y: self.view.frame.size.height - 50,
                                         width: self.view.frame.size.width,
                                         height: 50
                                     )
                                   )

self.view.addSubview(pageController)

remember use pageController.numberOfPages and delegate the pageView

then remove

func presentationCountForPageViewController(
    pageViewController: UIPageViewController
) -> Int

and

func presentationIndexForPageViewController(
    pageViewController: UIPageViewController
) -> Int

Then add another delegate method:

func pageViewController(
    pageViewController: UIPageViewController,
    willTransitionToViewControllers pendingViewControllers:[UIViewController]){
       if let itemController = pendingViewControllers[0] as? PageContentViewController {
           self.pageController.currentPage = itemController.pageIndex
       }
    }
}
Bernhard
  • 4,855
  • 5
  • 39
  • 70
Anibal R.
  • 798
  • 9
  • 12
3

Yes, the Shameerjan answer is very good, but instead of adding another page control you can use default page indicator:

- (UIPageControl*)pageControl {
for (UIView* view in self.view.subviews) {
    if ([view isKindOfClass:[UIPageControl class]]) {
        //set new pageControl position
        view.frame = CGRectMake( 100, 200, width, height);
        return (id)view;
    }
}
return nil;
}

and then extend the size of the UIPageViewController to cover up the bottom gap:

//somewhere in your code
self.view.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height+40);
Max Niagolov
  • 684
  • 9
  • 26
3

here s a very effective way to change the position of the default PageControl for the PageViewController without having the need to create a new one ...

extension UIPageViewController {
override open func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    for subV in self.view.subviews {
        if type(of: subV).description() == "UIPageControl" {
            let pos = CGPoint(x: subV.frame.origin.x, y: subV.frame.origin.y - 75 * 2)
            subV.frame = CGRect(origin: pos, size: subV.frame.size)
        }
    }
}
}
Nikolay Kostov
  • 16,433
  • 23
  • 85
  • 123
Hussein Dimessi
  • 377
  • 3
  • 8
1

Here's my take. This version only changes de indicator when the animation is finished, i.e. when you get to the destination page.

/** Each page you add to the UIPageViewController holds its index */
class Page: UIViewController {
    var pageIndex = 0
}

class MyPageController : UIPageViewController {

    private let pc = UIPageControl()
    private var pendingPageIndex = 0

    // ... add pc to your view, and add Pages ... 

    /** Will transition, so keep the page where it goes */
    func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController])
    {
        let controller = pendingViewControllers[0] as! Page
        pendingPageIndex = controller.pageIndex
    }

    /** Did finish the transition, so set the page where was going */
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool)
    {
        if (completed) {
            pc.currentPage = pendingPageIndex
        }
    }
}
Ferran Maylinch
  • 10,919
  • 16
  • 85
  • 100
1

Swift 5 & iOS 13.5 (with Manual Layout)

The example below uses a custom UIPageControl, laid out at the bottom center position of the UIPageViewController. To use the code, replace MemberProfilePhotoViewController with the type of view controller you are using as pages.

Quick Notes

  1. You must set the numberOfPages property on pageControl.
  2. To size pageControl you can use pageControl.size(forNumberOfPages: count).
  3. Make sure you delete presentationCount(...) and presentationIndex(...) to remove the default page control.
  4. Using UIPageViewControllerDelegate's didFinishAnimating seems to be better timing for updating pageControl.currentPage.

The Code

import Foundation
import UIKit

class MemberProfilePhotosViewController: UIPageViewController {
    
    private let profilePhotoURLs: [URL]
    private let profilePhotoViewControllers: [MemberProfilePhotoViewController]
    private var pageControl: UIPageControl?
    
    // MARK: - Initialization
    
    init(profilePhotoURLs: [URL]) {
        self.profilePhotoURLs = profilePhotoURLs
        profilePhotoViewControllers = profilePhotoURLs.map { (profilePhotoURL) -> MemberProfilePhotoViewController in
            MemberProfilePhotoViewController(profilePhotoURL: profilePhotoURL)
        }
        super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError()
    }
    
    // MARK: UIViewController
    
    override func loadView() {
        super.loadView()
        pageControl = UIPageControl(frame: CGRect.zero)
        pageControl!.numberOfPages = profilePhotoViewControllers.count
        self.view.addSubview(pageControl!)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        dataSource = self
        delegate = self
        if let firstViewController = profilePhotoViewControllers.first {
            setViewControllers([firstViewController],
                               direction: .forward,
                               animated: true,
                               completion: nil)
        }
    }
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        if let pageControl = pageControl {
            let pageControlSize = pageControl.size(forNumberOfPages: profilePhotoViewControllers.count)
            pageControl.frame = CGRect(
                origin: CGPoint(x: view.frame.midX - pageControlSize.width / 2, y: view.frame.maxY - pageControlSize.height),
                size: pageControlSize
            )
        }
    }
    
    // MARK: Private Helpers
    
    private func indexOf(_ viewController: UIViewController) -> Int? {
        return profilePhotoViewControllers.firstIndex(of: viewController as! MemberProfilePhotoViewController)
    }
}

extension MemberProfilePhotosViewController: UIPageViewControllerDelegate {
    func pageViewController(_ pageViewController: UIPageViewController,
                            didFinishAnimating finished: Bool,
                            previousViewControllers: [UIViewController],
                            transitionCompleted completed: Bool) {
        guard let selectedViewController = pageViewController.viewControllers?.first else { return }
        if let indexOfSelectViewController = indexOf(selectedViewController) {
            pageControl?.currentPage = indexOfSelectViewController
        }
    }
}

extension MemberProfilePhotosViewController: UIPageViewControllerDataSource {
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        guard let viewControllerIndex = profilePhotoViewControllers.firstIndex(of: viewController as! MemberProfilePhotoViewController) else {
            return nil
        }
        let previousIndex = viewControllerIndex - 1
        guard previousIndex >= 0 else {
            return nil
        }
        guard profilePhotoViewControllers.count > previousIndex else {
            return nil
        }
        return profilePhotoViewControllers[previousIndex]
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        guard let viewControllerIndex = profilePhotoViewControllers.firstIndex(of: viewController as! MemberProfilePhotoViewController) else {
            return nil
        }
        let nextIndex = viewControllerIndex + 1
        let profilePhotoViewControllersCount = profilePhotoViewControllers.count
        guard profilePhotoViewControllersCount != nextIndex else {
            return nil
        }
        guard profilePhotoViewControllersCount > nextIndex else {
            return nil
        }
        
        return profilePhotoViewControllers[nextIndex]
    }
}

Zorayr
  • 23,770
  • 8
  • 136
  • 129
0

This is good, and with more change

var pendingIndex = 0;
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    if completed {
        pageControl.currentPage = pendingIndex
    }
}

func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
    let itemController = pendingViewControllers.first as! IntroPageItemViewController
    pendingIndex = itemController.itemIndex
}
Community
  • 1
  • 1
M.R
  • 466
  • 6
  • 14
0

Make sure pageControl is added as subview. Then in

-(void)viewDidLayoutSubviews {
      [super viewDidLayoutSubviews];

      CGRect frame = self.pageControl.frame;
      frame.origin.x = self.view.frame.size.width/2 - 
                       frame.size.width/2;
      frame.origin.y = self.view.frame.size.height - 100 ;
      self.pageControl.numberOfPages = self.count;
      self.pageControl.currentPage = self.currentIndex;
      self.pageControl.frame = frame;
 }
Ali
  • 2,427
  • 22
  • 25
0
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    for view in self.view.subviews{
        if view is UIScrollView{
            view.frame = UIScreen.main.bounds
        }
        else if view is UIPageControl {
            view.backgroundColor = UIColor.clear
            view.frame.origin.y = self.view.frame.size.height - 75
        }
    }
}
Ing. Ron
  • 2,005
  • 2
  • 17
  • 33
-1

Just get the first view controller and find out the index in didFinishAnimating:

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

    pageControl.currentPage = onboardingViewControllers.index(of: pageViewController.viewControllers!.first!)!
}
Vrutin Rathod
  • 900
  • 1
  • 12
  • 16