UIPageViewController和故事板

52

我试图从故事板中特定地配置一个UIPageViewController:

enter image description here

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

问题是...

  • 第一页与页面指示器一起正常显示。在滑动时,
  • 我可以正常看到第二页。
  • 一旦过渡完成,我会得到一个黑屏(页面指示器正确显示第2页)。没有用户交互可用。

这是我第一次看到UIPageViewController本身用作其自身的数据源和委托。这似乎很合理,但我看过的每个教程都没有采用这种方法。希望你能成功。 - Travis M.
1
@BenjaminToueg,我很确定这里不需要使用“didMoveToParentViewController”。 - Fattie
这个较旧的问题本质上很简单,如果你对容器视图(container views)感到舒适 - 教程 - Fattie
8个回答

80
2023年,Swift已更新。
现在使用Storyboard非常容易做到这一点。
这种“滑动全屏教程”在一段时间内作为应用程序“介绍”很受欢迎,所以我将下面的类命名为IntroPages。
第一步,创建一个作为UIPageViewController的容器视图。
如果对iOS不熟悉,这里有一个容器视图教程。
(注意:如果不知道如何将容器视图更改为UIPageViewController,请在该教程中向下滚动到“如何更改...”部分!)

enter image description here )

你可以将容器制作成任何你想要的形状。就像任何容器视图一样,它可以是全屏或屏幕的一小部分 - 你想要什么就可以。
第二步,
制作四个简单普通的视图控制器,可以是任何你想要的东西 - 图像、文本、表格,任何东西都可以。(在示例中是紫色。)

four controllers

请注意,它们只是简单地在你的故事板上存在,不要将它们与任何东西链接起来。
第三步,你必须为这四个页面设置ID。使用"id1"、"id2"、"id3"、"id4"即可。

set the IDs

第四步,复制粘贴!这是IntroPages类,
更新至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
    }
}

(看一下注释 - 有代码可以选择循环或线性分页。)
在Storyboard上查看UIPageViewController。将类设置为IntroPages。
就是这样 - 完成了。
你只需在Storyboard上设置转场样式,

enter image description here

很有可能你想要的是“滚动”,而不是其他的那个。

关于使用正常的“.scroll”模式的重要注意事项...

令人难以置信的是,苹果在Xcode故事板的下拉菜单中删除了正常的“.scroll”选项。

(在故事板上,他们只允许你选择奇怪的选项,比如“curl”,而这个选项几乎没有人使用。)

现在你必须在代码中处理这个问题,这很简单。这一行代码解决了这个问题:

class IntroPages: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {

   required init?(coder: NSCoder) {
       super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
   }

这有点傻,但事实就是这样。(更多信息。)

令人惊讶的是,你已经完成了!

有点困惑,因为没有其他事情可做。

点击运行,它现在会起作用。

你可以去吃午饭,没有其他事情可做。

看着上面的第一张大图,"粉色"控制器 "aa","bb","cc" 等等...只需按照你希望的任何正常 iOS 视图控制器布局来制作。

现在是困难的部分...


添加UIPageControl的介绍...

你需要在上面的示例图中的最高级包装类"Intro"中添加UIPageControl

(所以,出乎意料的是,不是在页面视图控制器中,也不是在"IntroPages"中。)

因此,在Storyboard中,只需简单地将UIPageControl拖放到"Intro"上即可。

注意!奇怪的是,在Storyboard中,你无法移动UIPageControl

当你将一个页面控制器拖放到一个视图控制器上时,它只会固定在一个位置。这完全是奇怪的,但事实就是如此。不要浪费时间试图移动它 :) 如果你想根据UI指南重新定位它,只需在你希望的位置放置一个空的UIView,并将页面控制器放在这个容器视图中。

从"Intro"中,你需要按照通常的方式访问页面视图控制器(IntroPages):

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://dev59.com/KGAg5IYBdhLWcg3wm7_6#23403979
            pageViewController = (segue.destination as! IntroPages)
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.bringSubviewToFront(pageControl)
        pageControl.currentPage = 0
    }

请注意这个关键句子:
    view.bringSubviewToFront(pageControl)

添加这个功能
@IBAction func userDidChangePageControl(_ sender: UIPageControl) {}

然后在storyboard中点击页面控件并将valueChanged拖动到该函数中。
该函数的最简单的概要版本是...
@IBAction func userDidChangePageControl(_ sender: UIPageControl) {
    let newIndex = sender.currentPage
    pageViewController.setViewControllers(
        [pageViewController.pages[newIndex]],
        direction: (pageViewController.currentIndex < newIndex)
                     ? .forward : .reverse,
        animated: true, completion: nil)
}

...在IntroPages中,你需要添加该属性...
var currentIndex: Int {
    if let visibleViewController = viewControllers?.first,
       let ci = pages.firstIndex(of: visibleViewController) {
        return ci
    }
    else {
        return 0
    }
}

...并且在IntroPages中还要添加以下内容,这样,当用户滑动屏幕时,UIPageControl就知道该做什么了...
func pageViewController(_ pageViewController: UIPageViewController, 
       didFinishAnimating finished: Bool,
       previousViewControllers: [UIViewController], 
       transitionCompleted completed: Bool) {
    (parent as? Intro)?.pageControl.currentPage = currentIndex
}

那就可以了,但有两点需要注意。
1. 请参考https://stackoverflow.com/questions/66545624,详细了解页面控制器上苹果界面的精妙细节。
2. 2023年? 传统上,在IntroPages中,由于苹果的一个错误/异常行为,您需要添加以下内容。不清楚在最新的iOS版本中是否仍然需要这样做,需要注意的是平板电脑和手机的行为可能会有很大的不同。
传统上需要添加的内容为:
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) }
    }
} 

1
太棒了!这个效果非常好!现在只需要添加页面指示器和一种方法来禁用翻页效果,以便简单地滑入到下一个视图。 - Drew
2
我们无法通过编程的方式将transitionStyle更改为Scroll From page curl。为了解决这个问题,拖动一个UIPageViewController,将其连接到容器(作为嵌入),并在属性检查器中更改过渡样式。不要忘记更改类名。 - Ajumal
1
@JoeBlow 你好,我按照你的指示操作了。IntroPages已经出现了,但是我无法滑动它!!我有什么遗漏吗?? - Tai Dao
在Storyboard中是否可以处理“页面”数组的实现?如果流程中有15个视图控制器,实例化所有这些控制器并将它们存储在数组中可能会导致内存问题和应用程序崩溃。 - Satyam
@drew 希望这些年过得不错 :) 对于 Drew 或任何阅读此文的人,我已经添加了一篇关于如何开始使用 UIPageControl(即底部的“点”)的文章。 - Fattie
显示剩余3条评论

22

对于想要查看工作页面滚动(向前/向后)的人

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

补充EditOR的出色答案,如果您喜欢“循环翻页”的方式,则可以使用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];
    } 

我们有没有办法在页面视图控制器中检查当前页面的索引号? - G.Abhisek

11

借鉴Joe Blow的答案,并使用Swift代码扩展UIPageViewController类:

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

阅读有关使用带有容器视图的故事板设置中的 UIPageViewController 的更多信息。


这很方便,谢谢。需要注意的是,以上内容适用于其他答案中提到的“循环翻页”(向右翻页最终会重新开始而不是停在末尾)。 - Clifton Labrum
优秀的,@samwize - Fattie

4

在Storyboard中,关于UIPageViewController存在许多疑问。

这里提供了一些演示代码,展示如何将UIPageViewController用作独立的全屏视图或作为UIContainerView。如果您只想分页屏幕的一小部分区域,则可以这样做。

enter image description here enter image description here enter image description here


0

已更新至 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
    }
}

0
要使用@samwize的答案实现无限滚动,您需要添加条件来检查负值。否则,您只会在第一页和第二页之间切换。仅当您计划拥有超过两个页面时才需要这样做。
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]
    }

-1
问题在于您不正确地重复使用UIViewController实例:
myViewControllers = @[tuto1, tuto2, tuto1, tuto2];

我建议您拥有一个NSMutableSet,它将作为可重用的UIViewController实例的池。

viewControllerBeforeViewController:viewControllerBeforeViewController:中,使用NSPredicate搜索您的NSMutableSet,以查找parentViewController等于nilUIViewController。如果找到一个,则返回它。如果没有,则实例化一个新的,将其添加到NSMutableSet中,然后返回它。

当您完成测试并通过测试时,可以将该池提取到自己的类中。


1
这似乎不正确,Rudolf。只使用普通数组就可以完美地工作。没有任何问题。所有四个VC都只是“存在”——这不是问题。 - Fattie
@JoeBlow,是数组还是集合,由你决定。重要的是算法,特别是UIPageViewController成为parentViewController的事实。基于此,您可以过滤数组/集合并找到未使用的UIViewController实例。 - Rudolf Adamkovič

-2

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接