Reputation: 10867
I'm trying to configure a UIPageViewController SPECIFICALLY from storyboard:
TutorialPageViewController.h
#import <UIKit/UIKit.h>
@interface TutorialPageViewController : UIPageViewController <UIPageViewControllerDelegate, UIPageViewControllerDataSource>
@end
TutorialPageViewController.m
#import "TutorialPageViewController.h"
@interface TutorialPageViewController ()
@property (assign, nonatomic) NSInteger index;
@end
@implementation TutorialPageViewController
{
NSArray *myViewControllers;
}
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
self.delegate = self;
self.dataSource = self;
[self didMoveToParentViewController:self];
UIStoryboard *tutorialStoryboard = [UIStoryboard storyboardWithName:@"TutorialStoryboard" bundle:[NSBundle mainBundle]];
UIViewController *tuto1 = [tutorialStoryboard instantiateViewControllerWithIdentifier:@"TutorialPageViewController_1"];
UIViewController *tuto2 = [tutorialStoryboard instantiateViewControllerWithIdentifier:@"TutorialPageViewController_2"];
myViewControllers = @[tuto1, tuto2, tuto1, tuto2];
self.index = 0;
[self setViewControllers:@[tuto1] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}
- (UIViewController *)viewControllerAtIndex:(NSUInteger)index {
return myViewControllers[index];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
NSUInteger index = self.index;
if (index == 0) { return nil; }
// Decrease the index by 1 to return
index--;
return [self viewControllerAtIndex:index];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
NSUInteger index = self.index;
index++;
if (index > [myViewControllers count]) { return nil; }
return [self viewControllerAtIndex:index];
}
- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController {
// The number of items reflected in the page indicator.
return [myViewControllers count];
}
- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController {
// The selected item reflected in the page indicator.
return 0;
}
@end
Upvotes: 52
Views: 40077
Reputation: 12582
Nowadays it is dead easy to do this simply using Storyboard.
These sort of "swiping full-screen intros" 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,
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.
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 ...
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 sits in a fixed place which cannot be changed in any way. This is bizarre, but that's how it is. To position as you wish, just have a holder (an empty plain UIView). Put the holder where you wish. Sit the page control in side that holder.
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://stackoverflow.com/a/23403979/294884
pageViewController = (segue.destination as! IntroPages)
}
}
override func viewDidLoad() {
super.viewDidLoad()
pageControl.currentPage = 0
}
(NOTE. In older iOS you had to: view.bringSubviewToFront(pageControl)
in viewDidLoad due to a UIKit bug. From about iOS 15 this seems to be resolved.)
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)
}
(If you prefer, put that function in IntroPages, and, from the IBAction in Intro, just call down to it.)
Next, in IntroPages you must add the property:
var currentIndex: Int {
if let visibleViewController = viewControllers?.first,
let ci = pages.firstIndex(of: visibleViewController) {
return ci
}
else {
return 0
}
}
Next, in IntroPages you must add the following:
func pageViewController(_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
(parent as? Intro)?.pageControl.currentPage = currentIndex
}
That will do it.
But here are two points:
See https://stackoverflow.com/questions/66545624 regarding the exquisite details of the Apple page controller UX.
Traditionally you had to add the following code in viewDidLayoutSubviews
to counter a UIKit bug. However as of about iOS 15 the issue seems to be resolved.
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) }
}
}
Upvotes: 80
Reputation: 21881
Answer for Swift 5:
import UIKit
class MyPageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var allViewControllers = [UIViewController]()
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
let vc1: UIViewController! = storyboard?.instantiateViewController(withIdentifier: "viewController1")
let vc2: UIViewController! = storyboard?.instantiateViewController(withIdentifier: "viewController2")
allViewControllers = [vc1, vc2]
self.setViewControllers([vc1], direction: UIPageViewController.NavigationDirection.forward, animated: false)
}
// UIPageViewControllerDataSource
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController?
{
let currentIndex = allViewControllers.firstIndex(of: viewController)!
return currentIndex == 0 ? nil : allViewControllers[currentIndex-1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?
{
let currentIndex = allViewControllers.firstIndex(of: viewController)!
return currentIndex == allViewControllers.count-1 ? nil : allViewControllers[currentIndex+1]
}
func presentationCount(for pageViewController: UIPageViewController) -> Int
{
return allViewControllers.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int
{
return 0
}
}
Upvotes: -2
Reputation: 448
To have infinite scroll back using @samwize's answer you need to add conditional to check for negative values. Otherwise you just switch between the first and second page. This is only necessary if you plan on having more than two pages.
func pageViewController(_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
let currentIndex = pages.index(of: viewController)!
var previousIndex = (currentIndex - 1) % pages.count
if previousIndex < 0 {previousIndex = pages.count - 1}
return pages[previousIndex]
}
Upvotes: 0
Reputation: 14321
There seems to be a lot of questions regarding UIPageViewController in Storyboard.
Here is some demo code to show you how you can use the UIPageViewController in storyboard as a standalone full screen view or as a UIContainerView, if you want to page only a small area of your screen.
Upvotes: 4
Reputation: 451
Updated for Swift 3:
class YourPageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var pages = [UIViewController]()
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.dataSource = self
let page1: UIViewController! = storyboard?.instantiateViewController(withIdentifier: "page1")
let page2: UIViewController! = storyboard?.instantiateViewController(withIdentifier: "page2")
pages.append(page1)
pages.append(page2)
setViewControllers([page1], direction: UIPageViewControllerNavigationDirection.forward, animated: false, completion: nil)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let currentIndex = pages.index(of: viewController)!
let previousIndex = abs((currentIndex - 1) % pages.count)
return pages[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let currentIndex = pages.index(of: viewController)!
let nextIndex = abs((currentIndex + 1) % pages.count)
return pages[nextIndex]
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
return pages.count
}
}
Upvotes: 0
Reputation: 27353
Extending Joe Blow's answer with Swift code for the UIPageViewController
class:
import UIKit
class MyPageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var pages = [UIViewController]()
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.dataSource = self
let page1: UIViewController! = storyboard?.instantiateViewControllerWithIdentifier("page1")
let page2: UIViewController! = storyboard?.instantiateViewControllerWithIdentifier("page2")
pages.append(page1)
pages.append(page2)
setViewControllers([page1], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
let currentIndex = pages.indexOf(viewController)!
let previousIndex = abs((currentIndex - 1) % pages.count)
return pages[previousIndex]
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
let currentIndex = pages.indexOf(viewController)!
let nextIndex = abs((currentIndex + 1) % pages.count)
return pages[nextIndex]
}
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
return pages.count
}
func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int {
return 0
}
}
Read more on using UIPageViewController with container view with storyboard setup.
Upvotes: 11
Reputation: 367
For someone, who wants to see working page scroll (forward / backward)
-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerBeforeViewController:(UIViewController *)viewController
{
NSUInteger currentIndex = [myViewControllers indexOfObject:viewController];
// get the index of the current view controller on display
if (currentIndex > 0)
{
return [myViewControllers objectAtIndex:currentIndex-1];
// return the previous viewcontroller
} else
{
return nil;
// do nothing
}
}
-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerAfterViewController:(UIViewController *)viewController
{
NSUInteger currentIndex = [myViewControllers indexOfObject:viewController];
// get the index of the current view controller on display
// check if we are at the end and decide if we need to present
// the next viewcontroller
if (currentIndex < [myViewControllers count]-1)
{
return [myViewControllers objectAtIndex:currentIndex+1];
// return the next view controller
} else
{
return nil;
// do nothing
}
}
Just to add to this great answer by EditOR, here's what you do if you prefer "round and around" paging: still using the same technique of EditOR
-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerBeforeViewController:(UIViewController *)viewController
{
NSUInteger currentIndex = [myViewControllers indexOfObject:viewController];
--currentIndex;
currentIndex = currentIndex % (myViewControllers.count);
return [myViewControllers objectAtIndex:currentIndex];
}
-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerAfterViewController:(UIViewController *)viewController
{
NSUInteger currentIndex = [myViewControllers indexOfObject:viewController];
++currentIndex;
currentIndex = currentIndex % (myViewControllers.count);
return [myViewControllers objectAtIndex:currentIndex];
}
Upvotes: 22
Reputation: 31486
The problem is that you're improperly reusing UIViewController
instances:
myViewControllers = @[tuto1, tuto2, tuto1, tuto2];
I would suggest you to have an NSMutableSet
that would serve as a pool of reusable UIViewController
instances.
In viewControllerBeforeViewController:
and viewControllerBeforeViewController:
search your NSMutableSet
using NSPredicate
to find a UIViewController
with parentViewController
equal to nil
. If you find one, return it. If not, instantiate a new one, add it to the NSMutableSet
and then return it.
When you're done and your tests are passing, you can extract the pool into its own class.
Upvotes: -1