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


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

# この記事でできること

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

# 完成品
![c115f-n85z0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/39791fc8-0332-bc2e-bea4-b70918d8e60f.gif)

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

# 実装方法
## UIPageViewControllerの実装
基本的に[こちら](https://qiita.com/Takeshi_Akutsu/items/dbf54df8e8a50e8ed5be)の記事を参考にしつつ，swift4以降に対応させていきます．
[UIPageViewController](https://developer.apple.com/documentation/uikit/uipageviewcontroller)
### 1.project作成
`create a new Xcode project`を選択
<img width="802" alt="スクリーンショット 2019-10-22 19.28.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/4e18d223-f3be-da96-3041-874ed50dc36d.png">

`single ViewApp`を選択
<img width="1400" alt="スクリーンショット 2019-10-22 19.29.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/2441eaee-fd83-c15a-a68b-3196aad96897.png">

お好みで`Create Git repository on my mac`にチェックを入れて`Create`
<img width="1400" alt="スクリーンショット 2019-10-22 19.29.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/971a49f7-586f-3340-858f-75e64f3bbd08.png">

`test_walkthrough`と命名して`next`
<img width="1400" alt="スクリーンショット 2019-10-22 19.29.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/34baaafb-1a94-1881-888e-6fd88646061e.png">

ファイルのプロジェクトの生成場所を選んだら作成は終了です．
<img width="1680" alt="スクリーンショット 2019-10-22 19.57.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/f331306f-7984-1b20-e619-e3f9874d6428.png">

---

### 2.PageViewControllerの実装（StoryBoard編）
1. `Library`から`PageViewController`をドラック&ドロップする
<img width="700" alt="スクリーンショット 2019-11-15 19.35.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/0312d893-5dfe-43a7-f0a9-ab11ff779169.png">

2. `Is Initial View Controller（StoryBoardの矢印）`を`PageViewController`に設定する
<img width="401" alt="スクリーンショット 2019-11-15 19.35.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/ee9ffcaa-c723-3767-99b7-14368ec0d416.png">

3. `PageViewController`の`Transition Style`を`Page Curl`->`Scroll`に変更
<img width="260" alt="スクリーンショット 2019-11-15 21.18.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/3829c965-01ad-eac4-2e01-b0509df2efec.png">

4. 表示するページの枚数分`ViewController`を配置する
<img width="817" alt="スクリーンショット 2019-11-15 19.36.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/2465c68a-f104-56be-965e-6efbe6f3997d.png">

5. それぞれのViewに`Image View`と`Label`を配置する
<img width="1031" alt="スクリーンショット 2019-11-15 19.52.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/915e6bbf-e8a5-6ca3-76a7-d13442bcd623.png">
6. それぞれのViewControllerとStoryBoardIDに名前をつける（下記画像は例）
<img width="624" alt="スクリーンショット 2019-11-15 19.59.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/99cd7cce-714c-9346-8b7a-58396facc02c.png">
7. それぞれに対応したファイルを作成する
<img width="733" alt="スクリーンショット 2019-11-15 20.03.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/96817a92-deb9-1ba0-2429-f7f0b0b4323c.png">
<img width="731" alt="スクリーンショット 2019-11-15 20.04.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/fc6d5150-5f50-9bc4-c6a0-70535c7e7f3a.png">
同様の手順で`BViewController.swift`，`CViewController.swift`を作成します．

---
### 3.PageViewControllerの実装（ViewController編）
1.まず`PageViewController`というswiftファイルを作り，StoryBoardのPageControllに紐付けする．  
この時，Subclassは`UIPageViewController`としておきます．
<img width="729" alt="スクリーンショット 2019-11-15 21.27.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/3f133c07-03a6-590f-24e5-fdf401d0db2d.png">

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

```swift: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を配列でまとめておきます．
また，現在のページを示す変数も用意しておきます．

```swift: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|

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

```swift: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/8ff768c5-f503-0ae5-1ad6-8f42ee7cc05f.gif)


5.pageControl（ページ数や現在ページの表示）の実装
こちらの2つを使います．
[func presentationCount](https://developer.apple.com/documentation/uikit/uipageviewcontrollerdatasource/1614095-presentationcount)
[func presentationIndex](https://developer.apple.com/documentation/uikit/uipageviewcontrollerdatasource/1614116-presentationindex)

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

```swift:PageController
class PageViewController: UIPageViewController {

    var pageViewControllers: [UIViewController] = []
    var nowPage: Int = 0
    //var currentPageでページ番号を管理
    var currentPage = 0
//以下省略~~~~~~~~~~~~~~
```

```swift:PageController
extension PageViewController : UIPageViewControllerDataSource, UIPageViewControllerDelegate {
    //全ページ数を返すメソッド
    func presentationCount(for pageViewController: UIPageViewController) -> Int {
        return pageViewControllers.count
    }
    //現在のページを返すメソッド
    func presentationIndex(for pageViewController: UIPageViewController) -> Int {
        return currentPage
    }
//以下省略~~~~~~~~~~~~~~
```
これで画面の下にpageControlが出たかと思われます．
<img width="464" alt="スクリーンショット 2019-11-16 15.27.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/b5f78e14-16e0-2b73-9a15-3fb90f084e0a.png">

## ボタンによる遷移の実装
いよいよボタンの実装に入ります．
### StoryBoardにボタンを配置する．
いい感じの場所にボタンを配置します．
<img width="798" alt="スクリーンショット 2019-11-16 15.30.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230705/e2b58703-0aa8-622d-f80a-2c9fd3a11743.png">
### ViewControllerに紐付ける．
今回は`nextMove`という名前をつけて紐付けました．

###ボタンの動作を記述する
クロージャを使ってボタンがタップされた時に，`onButtonTapped`が呼び出される
→`onButtonTapped`が呼び出された時に次のviewを指定して呼び出す．
という処理を書いていきます．

```swift:AViewController.swift
import UIKit

class AViewController: UIViewController {
    var onButtonTapped: () -> Void = {}
    @IBAction func nextMove(_ sender: Any) {
        onButtonTapped()
    }
}
```
同様に`BViewController.swift`，`BViewController.swift`にも追加します．

```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`を指定してあげれば実装できるでしょう．

```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.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）](https://qiita.com/Takeshi_Akutsu/items/dbf54df8e8a50e8ed5be)
[presentationCount(for:) (Apple Developer Document)](https://developer.apple.com/documentation/uikit/uipageviewcontrollerdatasource/1614095-presentationcount)
[presentationIndex(for:) (Apple Developer Document)](https://developer.apple.com/documentation/uikit/uipageviewcontrollerdatasource/1614116-presentationindex)
[【Swift】クロージャ（基本編）](https://qiita.com/funafuna/items/9be7653cf8d002bac4ec)