Help us understand the problem. What is going on with this article?

UIPageViewControllerとボタンを使ったウォークスルーの実装

はじめに

この記事はnitkitコンピュータ研究部AdventCalender2019,二日目の記事です
チュートリアル(walkthrough)にボタンでのページ移動を実装したかったのですが,良い記事が見つからなかったのでメモの意味も込めてこの記事を書いてます.

この記事でできること

  • UIPageViewControllerの実装
  • UIPageViewControllerにボタンを使用したページ遷移の実装

完成品

c115f-n85z0.gif

GitHubソースコード: https://github.com/tessy0901/test_walkthrough

実装方法

UIPageViewControllerの実装

基本的にこちらの記事を参考にしつつ,swift4以降に対応させていきます.
UIPageViewController

1.project作成

create a new Xcode projectを選択
スクリーンショット 2019-10-22 19.28.58.png

single ViewAppを選択
スクリーンショット 2019-10-22 19.29.08.png

お好みでCreate Git repository on my macにチェックを入れてCreate
スクリーンショット 2019-10-22 19.29.55.png

test_walkthroughと命名してnext
スクリーンショット 2019-10-22 19.29.22.png

ファイルのプロジェクトの生成場所を選んだら作成は終了です.
スクリーンショット 2019-10-22 19.57.13.png


2.PageViewControllerの実装(StoryBoard編)

  1. LibraryからPageViewControllerをドラック&ドロップする
    スクリーンショット 2019-11-15 19.35.30.png

  2. Is Initial View Controller(StoryBoardの矢印)PageViewControllerに設定する
    スクリーンショット 2019-11-15 19.35.50.png

  3. PageViewControllerTransition StylePage Curl->Scrollに変更
    スクリーンショット 2019-11-15 21.18.50.png

  4. 表示するページの枚数分ViewControllerを配置する
    スクリーンショット 2019-11-15 19.36.45.png

  5. それぞれのViewにImage ViewLabelを配置する
    スクリーンショット 2019-11-15 19.52.23.png

  6. それぞれのViewControllerとStoryBoardIDに名前をつける(下記画像は例)
    スクリーンショット 2019-11-15 19.59.12.png

  7. それぞれに対応したファイルを作成する
    スクリーンショット 2019-11-15 20.03.52.png
    スクリーンショット 2019-11-15 20.04.07.png
    同様の手順でBViewController.swiftCViewController.swiftを作成します.


3.PageViewControllerの実装(ViewController編)

1.まずPageViewControllerというswiftファイルを作り,StoryBoardのPageControllに紐付けする.

この時,SubclassはUIPageViewControllerとしておきます.
スクリーンショット 2019-11-15 21.27.11.png

2.中身を以下のようにする

PageViewController
import UIKit

class PageViewController: UIPageViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
        dataSource = self

        let pageViewA = storyboard!.instantiateViewController(withIdentifier: "AViewController") as! AViewController

        self.setViewControllers([pageViewA], direction: .forward, animated: true, completion: nil)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

setViewControllers()メソッドで最初のViewを読み込ませている

3.dataSourceを使い,前後のページの読み込みを行うようにする.

この時,複数のViewを配列でまとめておきます.
また,現在のページを示す変数も用意しておきます.

PageViewController
import UIKit

class PageViewController: UIPageViewController {

    var pageViewControllers: [UIViewController] = []
    var nowPage: Int = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        //インスタンス化
        let pageViewA = storyboard!.instantiateViewController(withIdentifier: "AViewController") as! AViewController
        let pageViewB = storyboard!.instantiateViewController(withIdentifier: "BViewController") as! BViewController
        let pageViewC = storyboard!.instantiateViewController(withIdentifier: "CViewController") as! CViewController
        pageViewControllers = [pageViewA, pageViewB, pageViewC]

        //最初に表示するページの指定
        self.setViewControllers([pageViewControllers[0]], direction: .forward, animated: true, completion: nil)

    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

//以下追加
extension PageViewController : UIPageViewControllerDataSource, UIPageViewControllerDelegate {

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        return nil
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        return nil
    }
}

追加した,
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController)
と,
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController)
がそれぞれ現在のページの前と後ろのページの指定になります.

4.前後ページの指定の実装
ページをまとめた配列のPageViewControllersと,現在のページを表すnowPageを使い,ページが変わるごとに前後のページを更新する動作を実装します.
下記の表のように実装していきます.

前のページ 現在のページ 次のページ
nil 0 nowPage+1
nowPage-1 0<現在のページ<ページ数-1 nowPage+1
nowPage-1 ページ数-1 nil

メソッドを以下のように変更します.

PageViewController
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
    let index = pageViewControllers.firstIndex(of: viewController)
    if index == 0 {
        return nil
    } else {
        return pageViewControllers[index!-1]
    }
}

func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
    let index = pageViewControllers.firstIndex(of: viewController)
    if index == pageViewControllers.count - 1 {
        return nil
    } else {
        return pageViewControllers[index!+1]
    }
}

ここまで実装できたらビルドして動作を確認します.
スワイプでページの移動はできるようになったものの,ページ数の表示ができていないのが確認できたと思います.
rijl0-lbqqd.gif

5.pageControl(ページ数や現在ページの表示)の実装
こちらの2つを使います.
func presentationCount
func presentationIndex

まずclass PageController内で,ページ番号を管理する変数を宣言します.
そして上記二つのメソッドをextensionに追加し,ページ数と現在のページ数の表示を行います,

PageController
class PageViewController: UIPageViewController {

    var pageViewControllers: [UIViewController] = []
    var nowPage: Int = 0
    //var currentPageでページ番号を管理
    var currentPage = 0
//以下省略~~~~~~~~~~~~~~
PageController
extension PageViewController : UIPageViewControllerDataSource, UIPageViewControllerDelegate {
    //全ページ数を返すメソッド
    func presentationCount(for pageViewController: UIPageViewController) -> Int {
        return pageViewControllers.count
    }
    //現在のページを返すメソッド
    func presentationIndex(for pageViewController: UIPageViewController) -> Int {
        return currentPage
    }
//以下省略~~~~~~~~~~~~~~

これで画面の下にpageControlが出たかと思われます.
スクリーンショット 2019-11-16 15.27.36.png

ボタンによる遷移の実装

いよいよボタンの実装に入ります.

StoryBoardにボタンを配置する.

いい感じの場所にボタンを配置します.
スクリーンショット 2019-11-16 15.30.02.png

ViewControllerに紐付ける.

今回はnextMoveという名前をつけて紐付けました.

ボタンの動作を記述する

クロージャを使ってボタンがタップされた時に,onButtonTappedが呼び出される
onButtonTappedが呼び出された時に次のviewを指定して呼び出す.
という処理を書いていきます.

AViewController.swift
import UIKit

class AViewController: UIViewController {
    var onButtonTapped: () -> Void = {}
    @IBAction func nextMove(_ sender: Any) {
        onButtonTapped()
    }
}

同様にBViewController.swiftBViewController.swiftにも追加します.

PageViewController
import UIKit

class PageViewController: UIPageViewController {

    var pageViewControllers: [UIViewController] = []
    var nowPage: Int = 0
    var currentPage = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
        dataSource = self

        //インスタンス化
        let pageViewA = storyboard!.instantiateViewController(withIdentifier: "AViewController") as! AViewController
        let pageViewB = storyboard!.instantiateViewController(withIdentifier: "BViewController") as! BViewController
        let pageViewC = storyboard!.instantiateViewController(withIdentifier: "CViewController") as! CViewController
        pageViewControllers = [pageViewA, pageViewB, pageViewC]

        //最初に表示するページの指定
        self.setViewControllers([pageViewControllers[0]], direction: .forward, animated: true, completion: nil)
//ここから
        pageViewA.onButtonTapped = {
            self.setViewControllers([self.pageViewControllers[1]], direction: .forward, animated: true, completion: nil)
        }
        pageViewB.onButtonTapped = {
            self.setViewControllers([self.pageViewControllers[2]], direction: .forward, animated: true, completion: nil)
        }
        pageViewC.onButtonTapped = {
            self.setViewControllers([self.pageViewControllers[0]], direction: .forward, animated: true, completion: nil)
        }
    }
//ここまで追加
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

これでボタンの遷移はできるようになったはずですが,下のPageControlが動いていないと思うので動くようにしていきます.
なおスワイプではちゃんと動いているはずですのでそれも確認しておきましょう.

pageControlを動作させる

ページが変わった時にはpresentationIndexメソッドが呼び出されるのですが,そこにはcurrentPageが返されるように記述しているはずです.
ですので,ボタンをタップした時に次のcurrentPageを指定してあげれば実装できるでしょう.

PageViewController
import UIKit

class PageViewController: UIPageViewController {

    var pageViewControllers: [UIViewController] = []
    var nowPage: Int = 0
    var currentPage = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
        dataSource = self

        //インスタンス化
        let pageViewA = storyboard!.instantiateViewController(withIdentifier: "AViewController") as! AViewController
        let pageViewB = storyboard!.instantiateViewController(withIdentifier: "BViewController") as! BViewController
        let pageViewC = storyboard!.instantiateViewController(withIdentifier: "CViewController") as! CViewController
        pageViewControllers = [pageViewA, pageViewB, pageViewC]

        //最初に表示するページの指定
        self.setViewControllers([pageViewControllers[0]], direction: .forward, animated: true, completion: nil)

        pageViewA.onButtonTapped = {
            //追加
            self.currentPage = 1
            self.setViewControllers([self.pageViewControllers[1]], direction: .forward, animated: true, completion: nil)
        }
        pageViewB.onButtonTapped = {
            //追加
            self.currentPage = 2
            self.setViewControllers([self.pageViewControllers[2]], direction: .forward, animated: true, completion: nil)
        }
        pageViewC.onButtonTapped = {
            //追加
            self.currentPage = 0
            self.setViewControllers([self.pageViewControllers[0]], direction: .forward, animated: true, completion: nil)
        }
    }

これでビルドするとボタンを押した時にpageControlも移動してるのが確認できたと思います.

最後に

今回は表示するViewごとにそれぞれswiftファイルを作成しましたが,ボタン遷移だけなら一つのviewを使いまわしても問題ないです.
もっといい書き方があると思うので,コメントいただけるとありがたいです.
なお,3番目のviewにボタンがないのは寂しい(?)という理由で,ボタンをタップした時のみループするようにしています.

次はUIScrolViewを使って同じ機能の実装方法を紹介したいと思います.

参考文献

【決定版】UIPageViewControllerの使い方(Swift)
presentationCount(for:) (Apple Developer Document)
presentationIndex(for:) (Apple Developer Document)
【Swift】クロージャ(基本編)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした