2023. Swift updated.
Nowadays it is dead easy to do this simply using Storyboard.
These sort of "swiping full-screen tutorials" were popular as app "intros" for awhile, so I called the class below IntroPages
.
Step 1, make a container view that is a UIPageViewController.
If new to iOS, here is a container view tutorial.
( Note: if you don't know how to "change" the container view to a UIPageViewController, scroll down to the section "How to change..." on that tutorial!
)
You can make the container any shape you want. As with any container view, it can be full-screen or a small part of the screen - whatever you want.
Step 2,
Make four straightforward, ordinary, view controllers which can be anything you want - images, text, tables, anything at all. (Purple in the example.)
Note that they simply sit there on your storyboard, do not link them to anything.
Step 3, you must Set the IDs of those four pages. "id1", "id2", "id3", "id4" is fine.
Step 4, copy and paste! Here's the class IntroPages,
Updated for 2023 ...
import UIKit
class IntroPages: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
// (see note below about ".scroll" mode, you almost always need this line of code:)
required init?(coder: NSCoder) {
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
}
var pages = [UIViewController]()
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.dataSource = self
let p1: UIViewController! = storyboard?.instantiateViewController(withIdentifier: "id1")
let p2: UIViewController! = storyboard?.instantiateViewController(withIdentifier: "id2")
let p3: UIViewController! = storyboard?.instantiateViewController(withIdentifier: "id3")
// etc ...
pages.append(p1)
pages.append(p2)
pages.append(p3)
// etc ...
setViewControllers([p1], direction: .forward, animated: false, completion: nil)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController)-> UIViewController? {
guard let cur = pages.firstIndex(of: viewController) else { return nil }
// if you prefer to NOT scroll circularly, simply add here:
// if cur == 0 { return nil }
var prev = (cur - 1) % pages.count
if prev < 0 {
prev = pages.count - 1
}
return pages[prev]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController)-> UIViewController? {
guard let cur = pages.firstIndex(of: viewController) else { return nil }
// if you prefer to NOT scroll circularly, simply add here:
// if cur == (pages.count - 1) { return nil }
let nxt = abs((cur + 1) % pages.count)
return pages[nxt]
}
func presentationIndex(for pageViewController: UIPageViewController)-> Int {
return pages.count
}
}
(Look at the comments - there is code for either looping or linear paging as you prefer.)
On the storyboard look at the UIPageViewController. Set the class to be IntroPages
.
That's all there is to it - you're done.
You simply set the transition style on the storyboard,
it is very likely you want "Scroll", not the other one.
Important NOTE on using the normal, usual, ".scroll" mode ...
Incredibly bizarrely, Apple have removed the normal ".scroll" option from the drop-down in IB in Xcode storyboard.
(On storyboard, they only allow you to choose the bizarre options like "curl", which nobody ever uses.)
You now have to do this in code, which is simple. This line takes care of the problem:
class IntroPages: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
required init?(coder: NSCoder) {
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
}
It's silly, but that's how it is. (More info.)
Surprisingly, you're done!
It's a bit confusing, because there's nothing else to do.
Hit run and it will now work.
You can go to lunch, there's nothing else to do.
Looking at the first large image above, the "pink" controllers "aa", "bb", "cc" etc ... simply make those any way you wish, as any normal view controller layout in iOS.
And now the hard part ...
Introduction to adding UIPageControl ...
You add the UIPageControl
in the highest-level wrapper class, "Intro" in the above image example.
(So, surprisingly not in the page view controller, not in "IntroPages".)
Thus, on the storyboard, very simply drag a UIPageControl
on to "Intro".
Note! Bizarrely, in storyboards, you cannot move a UIPageControl.
When you drag a page control on to a view controller, it just sits in a fixed place. This is completely bizarre but that's how it is. Don't waste time trying to move it :) If you want to reposition it against UI guidelines, just have an empty UIView where you wish, and sit the page control in side that holder view.
From Intro, you will need to access the page view controller (IntroPages) in the usual way:
class Intro: UIViewController {
@IBOutlet var pageControl: UIPageControl!
var pageViewController: IntroPages!
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segueIntroPages" {
// if new to container views, identifier explained here:
// https://mcmap.net/q/17273/-how-to-add-a-subview-that-has-its-own-uiviewcontroller-in-objective-c
pageViewController = (segue.destination as! IntroPages)
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.bringSubviewToFront(pageControl)
pageControl.currentPage = 0
}
Note this key line:
view.bringSubviewToFront(pageControl)
Add this function
@IBAction func userDidChangePageControl(_ sender: UIPageControl) {}
and then in storyboard click on the page control and drag valueChanged to that function.
The simplest outline version of the function is ...
@IBAction func userDidChangePageControl(_ sender: UIPageControl) {
let newIndex = sender.currentPage
pageViewController.setViewControllers(
[pageViewController.pages[newIndex]],
direction: (pageViewController.currentIndex < newIndex)
? .forward : .reverse,
animated: true, completion: nil)
}
... and in IntroPages you have to add the property ...
var currentIndex: Int {
if let visibleViewController = viewControllers?.first,
let ci = pages.firstIndex(of: visibleViewController) {
return ci
}
else {
return 0
}
}
... and in IntroPages also add the following, so that, when the user swipes the screen the UIPageControl knows what to do ...
func pageViewController(_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
(parent as? Intro)?.pageControl.currentPage = currentIndex
}
That will do it, but two points.
See https://stackoverflow.com/questions/66545624 for a fuller investigation of the exquisite details of the Apple UI on the page controller.
2023? Traditionally in IntroPages you have to add the following due to an Apple bug / unusual behavior. It's unclear if this is still needed in the most recent iOS, noting that pad/phone seem to have quite different behaviors.
Traditionally had to add:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let subViews = view.subviews
var scrollView: UIScrollView? = nil
var pageControl: UIPageControl? = nil
// maintain this code order...
for view in subViews {
if view.isKind(of: UIScrollView.self) {
scrollView = view as? UIScrollView
}
else if view.isKind(of: UIPageControl.self) {
pageControl = view as? UIPageControl
}
}
// maintain this code order...
if (scrollView != nil && pageControl != nil) {
scrollView?.frame = view.bounds
if let pageControl = pageControl {
view.bringSubviewToFront(pageControl) }
}
}