Build a paged UIScrollView with UIPageControl, plus iOS 14 upgrades

This article covers two layers of the same UIKit pattern. First, it shows how to build a multi-page horizontal interface with UIScrollView and UIPageControl. Then it moves into the iOS 14 additions that made UIPageControl far more customizable, including custom indicator images, visible background styles, and continuous scrolling interaction.

Paged UIKit screen with a prominent UIPageControl at the bottom

The article is really two tutorials in one: a classic paged-scroll UIKit setup, and a tour of the new UIPageControl APIs that arrived in iOS 14.

The first half shows the manual way to place multiple views side by side inside a horizontal UIScrollView, then keep a UIPageControl in sync with the current page. The second half focuses on what changed in iOS 14: you can now change the indicator art, choose visible background treatments, and let the user scrub across the control itself.

Animated demo of a paged UIScrollView controlled by UIPageControl
The core pattern is a horizontally paged scroll view with a page control floating above it.
Scope This article stays in UIKit and builds the whole pattern programmatically instead of using Interface Builder.

The sample starts with three stored properties: the page views, the scroll view, and the page control.

That is the right mental model for this kind of UI. One array owns the individual page content, one scroll view provides the paging behavior, and one page control acts as the visible page index and as an alternate input method.

private var views = [UIView]()
private var scrollView: UIScrollView!
private var pageControl: UIPageControl!

This article also links a finished sample project if you want the complete version directly: PageViewExample on GitHub.

The scroll view setup is minimal: create it to match the screen, turn paging on, assign the delegate, and add it to the view hierarchy.

scrollView = UIScrollView(frame: self.view.frame)
scrollView.isPagingEnabled = true
scrollView.backgroundColor = .clear
scrollView.delegate = self
self.view.addSubview(scrollView)

The page content itself is then laid out manually in a loop. Each child view gets a frame whose x position is offset by one screen width, which turns the array into a horizontal strip of pages.

The article uses a simple fixed example for the page frames, which keeps the geometry easy to follow even if the layout is not meant to be production-polished.

The page control is configured from the same source of truth as the scroll view: the number of page views already prepared in the array.

pageControl = UIPageControl(
    frame: CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: 50))
)
pageControl.numberOfPages = views.count
pageControl.currentPage = 0
pageControl.addTarget(self, action: #selector(self.pageUpdated), for: .valueChanged)
pageControl.pageIndicatorTintColor = .gray
pageControl.currentPageIndicatorTintColor = .white
view.addSubview(pageControl)
view.bringSubviewToFront(pageControl)

This is the other half of the pattern: once the control knows how many pages exist, it can both reflect the current page and send events when the user taps a dot.

Page control attached to a horizontally paged scroll view
The page control sits above the paged content and is configured from the same page-count data.

The remaining wiring is about geometry and synchronization.

In the layout step, the sample recalculates frames and sets the scroll view content width so the pages can actually scroll. The article's viewWillLayoutSubviews section shows this explicitly with a computed total width and a matching contentSize.

From there, the usual two-way sync applies:

The scroll view delegate updates pageControl.currentPage as the user swipes, and the page control target method scrolls to the selected page when the user interacts with the control itself.

Implementation Note The source clearly shows the event target and layout code; the two-way sync behavior is the intended pattern implied by that structure and by the demo output.

After the base paging setup works, the article switches to the iOS 14 UIPageControl APIs that make the control feel much less generic.

The first addition is a global preferred indicator image:

pageControl.preferredIndicatorImage = UIImage(systemName: "star.fill")

If you want each page to use its own symbol, the article then switches to per-page images:

pageControl.setIndicatorImage(UIImage(systemName: "sun.max.fill"), forPage: 0)
pageControl.setIndicatorImage(UIImage(systemName: "cloud.sun.fill"), forPage: 1)
pageControl.setIndicatorImage(UIImage(systemName: "cloud.drizzle.fill"), forPage: 2)

The next addition is background visibility. iOS 14 introduced styles that make the control easier to read over busy content:

pageControl.backgroundStyle = .minimal
pageControl.backgroundStyle = .prominent
UIPageControl using the minimal background style in iOS 14
The new background styles let the control stay readable even when the page content behind it is visually noisy.

The last upgrade is interaction itself. By default, dragging across the page control does not scrub between pages. The iOS 14 property below changes that behavior:

pageControl.allowsContinuousInteraction = true
UIPageControl before continuous interaction is enabled
Without continuous interaction, dragging over the page control does not behave like a scrubber.
UIPageControl after continuous interaction is enabled
With continuous interaction enabled, the control becomes a faster page-selection surface.

The underlying paging architecture is old UIKit, but iOS 14 made the finishing details of UIPageControl much better.

That is the real value of this article. It does not just show how to wire a page control to a scroll view. It also shows how to stop the control from looking like the default dots every app used for years. With custom symbols, background styles, and scrub interaction, the same old paging pattern feels much more intentional.