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.
14 Answers
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;
}
-
1Thanks 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
-
5This 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
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
}
}
}

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

- 1,093
- 6
- 11
-
2This gives a really good control over everything and prevent the addition of a new pageControl. Thanks! – chrilith Apr 25 '16 at 16:51
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!

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

- 19,169
- 4
- 55
- 90
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
}
}
}
-
-
Value of type 'PageViewController' has no member 'pageController' – Jesus Rodriguez Jan 18 '17 at 17:39
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);

- 684
- 9
- 26
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)
}
}
}
}

- 16,433
- 23
- 85
- 123

- 377
- 3
- 8
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
}
}
}

- 10,919
- 16
- 85
- 100
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
- You must set the
numberOfPages
property onpageControl
. - To size
pageControl
you can usepageControl.size(forNumberOfPages: count)
. - Make sure you delete
presentationCount(...)
andpresentationIndex(...)
to remove the default page control. - Using
UIPageViewControllerDelegate
'sdidFinishAnimating
seems to be better timing for updatingpageControl.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]
}
}

- 23,770
- 8
- 136
- 129
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
}
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;
}

- 2,427
- 22
- 25
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
}
}
}

- 2,005
- 2
- 17
- 33
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!)!
}

- 900
- 1
- 12
- 16