iOSアプリ開発入門#4 ~UIPageViewController~

## 目的
- よく使うであろう（？）UIPageViewControllerを使用したアプリをStoryboardありとなしでサクッと作れるようになる
- ありなしの両方を試すことで、Storyboardが何をやっているのかなんとなくイメージできるようになる

## Step1. コードのみでUIPageViewController管理化
[前回のコード](https://qiita.com/chang1019/items/02b52c574a4600b22574) をベースに作業。
今回もなるべく爆速でUIPageViewControllerを利用するため、ひとまずStoryboardではなくコード側で実装してみる。
すでにUITabBarControllerで管理されたUINavigationControllerと無名UIViewControllerがあるため、これをUIPageViewControllerに置き換えてみる

- UITabBarController
    - tabBarItem1: UINavigationController
        - stack[]
            - ViewController(TableView)
            - ... add押下でViewControllerが複製されてstackに積まれていく
    - tabBarItem2: UIViewController ← ココをUIPageViewControllerに

### 1.1. PageViewControllerクラスの追加
swiftクラスファイルを追加し「PageViewController.swift」として保存する。
とりあえず固定3ページで単にページ番号を表示するだけのViewを表示するようなイメージで以下。

```swift:PageViewController.swift
import UIKit

class PageViewController:
    UIViewController,
    UIPageViewControllerDelegate,
    UIPageViewControllerDataSource
{
    var pages: [PageChildViewController] = [
        PageChildViewController(pageIndex: 0),
        PageChildViewController(pageIndex: 1),
        PageChildViewController(pageIndex: 2),
    ]

    override func viewDidLoad() {
        let pc: UIPageViewController = UIPageViewController(transitionStyle: .scroll,
                                                            navigationOrientation: .horizontal)
        pc.dataSource = self
        pc.delegate = self
        pc.setViewControllers([self.pages[0]],
                              direction: .forward,
                              animated: false,
                              completion: nil)
        self.addChild(pc)
        self.view.addSubview(pc.view)
    }

    // UIPageViewDataSource

    func pageViewController(_ pageViewController: UIPageViewController,
        viewControllerBefore viewController: UIViewController) -> UIViewController?
    {
        let child: PageChildViewController = viewController as! PageChildViewController
        let beforePageIndex: Int = child.pageIndex
        if 0 == beforePageIndex {
            return nil
        }
        return self.pages[beforePageIndex - 1]
    }

    func pageViewController(_ pageViewController: UIPageViewController,
        viewControllerAfter viewController: UIViewController) -> UIViewController?
    {
        let child: PageChildViewController = viewController as! PageChildViewController
        let afterPageIndex: Int = child.pageIndex
        if self.pages.count <= afterPageIndex + 1 {
            return nil
        }
        return self.pages[afterPageIndex + 1]
    }
}

class PageChildViewController: UIViewController {
    var pageIndex: Int = 0

    required init?(coder aDecoder: NSCoder) {
        fatalError("not implemented")
    }
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }

    convenience init(pageIndex: Int) {
        self.init(nibName: nil, bundle: nil)
        self.pageIndex = pageIndex
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = UIColor.white

        let label: UILabel = UILabel(frame: view.bounds)
        label.text = "page:\(pageIndex)"
        label.textAlignment = .center

        view.addSubview(label)
    }
}
```
ポイントとしては、

- UITableViewControllerとは異なり、addSubViewするだけでなく、子ViewControllerとしてUIPageViewControllerを格納する必要がある
- viewControllerBeforeで前のpageに戻った際のUIViewController、viewControllerAfterで次のpageに進んだ際のUIViewControllerを返却してやる
- インスタンス変数などでpageIndexを管理したくなるが、必ずしも同期がとれるわけではないので（内部pageIndexのみが変更し、実際の画面がスワイプしきれない場合がある）、必ず引数のviewControllerAfterやviewControllerBeforeに格納されている情報を利用する
    - 今回は全てのpageにそのpage自体のpageIndexを持たせているのでこちらを使用している

### 1.2. AppDelegateの修正
続いてAppDelegateの修正。
上記の通り、すでにTabBarやNavigtationの設定は済んでいる前提。

```diff:AppDelegate.swift
--- a/PracticeApp/AppDelegate.swift
+++ b/PracticeApp/AppDelegate.swift
@@ -17,6 +17,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
         // Override point for customization after application launch.
 
+        let tabc: UITabBarController = window?.rootViewController as! UITabBarController
+        var newVcs: [UIViewController] = []
+
+        // tab1: navigation controller
+        for vc in tabc.viewControllers! {
+            if vc is UINavigationController {
+                newVcs.append(vc)
+            }
+        }
+
+        // tab2: pageview controller
+        let pageVc: PageViewController = PageViewController()
+        pageVc.tabBarItem = UITabBarItem(tabBarSystemItem: .bookmarks, tag: 1)
+        newVcs.append(pageVc)
+
+        tabc.setViewControllers(newVcs, animated: true)
         return true
     }
```

UIPageViewControllerの設定自体はPageViewController内で実装しているので、ここでのポイントはあまりない。
Storyboard側で設定しているtabBar要素からUINavigationControllerの部分だけを再利用し、新たに自作のPageViewControllerを追加してtabに設定している。


### 1.3. 起動
Runすると、tabとnaviとpageが組み合わさった適当アプリが動く。
<img width="310" alt="スクリーンショット 2019-03-03 18.15.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/0b646d71-d012-00f3-582c-e24df4d7b84f.png">


## Step2. 動的paging+UIちょっと工夫
スワイプする分だけ（理論上）無限にpageが変わるように変更する。
また、ちょっと見た目もわかりやすく背景色をpage数によって変えてみる。

### 2.1. PageViewControllerの修正
以下のように修正

```diff:PageViewController.swift
--- a/PracticeApp/PageViewController.swift
+++ b/PracticeApp/PageViewController.swift
@@ -15,8 +15,6 @@ class PageViewController:
 {
     var pages: [PageChildViewController] = [
         PageChildViewController(pageIndex: 0),
-        PageChildViewController(pageIndex: 1),
-        PageChildViewController(pageIndex: 2),
     ]
 
     override func viewDidLoad() {
@@ -42,7 +40,7 @@ class PageViewController:
         if 0 == beforePageIndex {
             return nil
         }
-        return self.pages[beforePageIndex - 1]
+        return getPage(pageIndex: beforePageIndex - 1)
     }
 
     func pageViewController(_ pageViewController: UIPageViewController,
@@ -50,10 +48,21 @@ class PageViewController:
     {
         let child: PageChildViewController = viewController as! PageChildViewController
         let afterPageIndex: Int = child.pageIndex
-        if self.pages.count <= afterPageIndex + 1 {
+        return getPage(pageIndex: afterPageIndex + 1)
+    }
+
+    // private
+
+    func getPage(pageIndex: Int) -> PageChildViewController?
+    {
+        if pageIndex < 0 {
             return nil
         }
-        return self.pages[afterPageIndex + 1]
+        while self.pages.count <= pageIndex {
+            let vc: PageChildViewController = PageChildViewController(pageIndex: self.pages.count)
+            self.pages.append(vc)
+        }
+        return self.pages[pageIndex]
     }
 }
 
@@ -76,8 +85,22 @@ class PageChildViewController: UIViewController {
     override func viewDidLoad() {
         super.viewDidLoad()
 
-        view.backgroundColor = UIColor.white
+        // background color
+        var r = 0.1 * CGFloat(self.pageIndex)
+        while 1 < r {
+            r -= 1
+        }
+        var g = 0.2 * CGFloat(self.pageIndex)
+        while 1 < g {
+            g -= 1
+        }
+        var b = 0.3 * CGFloat(self.pageIndex)
+        while 1 < b {
+            b -= 1
+        }
+        self.view.backgroundColor = UIColor(red: r, green: g, blue: b, alpha: 1)
 
+        // label
         let label: UILabel = UILabel(frame: view.bounds)
         label.text = "page:\(pageIndex)"
         label.textAlignment = .center
```

ポイントとしては

- pagesでPageChildViewControllerのインスタンスを管理（再利用のみ、特に破棄などはしない）
- PageChildControllerはコンストラクト時の引数pageIndexに応じてbackgroundColorを変更

### 2.2. 起動
Runすると、スワイプに応じて背景色の変わるアプリが動く
<img width="310" alt="スクリーンショット 2019-03-03 18.32.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/4005b28c-7902-e398-a4a9-e4edc6f32aa2.png">


## Step3. Storyboardを使ってみる
これまで通り、今記述した部分をなるべくStoryboardにお任せする

### 3.1. UIPageViewControllerをStoryboardへ移行
まず不要なUIViewControllerを削除
<img width="1543" alt="スクリーンショット_2019-03-03_22_55_10.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/fb90b1c9-81b9-c06e-8b3c-90e6506b2527.png">

次にUIPageViewControllerを追加
<img width="500" alt="スクリーンショット 2019-03-03 22.55.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/cc223698-9c50-fd8c-a09f-7908d65c22fb.png">

TabBarControllerのoutletsの「view controllers」とUIPageViewControllerをバインドする
<img width="1543" alt="スクリーンショット_2019-03-03_22_56_03.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/937c25fc-b305-3713-7b5a-909606c4cc3f.png">

tabBarItemのSystem Itemsも設定しておく
<img width="1543" alt="スクリーンショット_2019-03-03_22_57_29.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/c156aefa-a06c-cd29-9d26-0f7a935fcd2a.png">

PageViewControllerの諸々の属性も設定しておく
<img width="1543" alt="スクリーンショット_2019-03-03_22_56_50.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/7a944597-9d90-fce4-44de-fad5659d5f48.png">

### 3.2. 子ページコントローラ（PageChildViewController)をStoryboardへ
新規にUIViewControllerを追加し、UILabelなどを配置
<img width="1543" alt="68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f313134342f32393239333961612d666237322d613335632d343564642d3833316161303131363230342e706e67.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1144/61a5a466-9622-6d59-ce33-d503013e8144.png">

IBOutletで各Labelをバインドするほか、クラス設定、Storyboard IDも忘れずに設定しておく
<img width="1543" alt="68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f313134342f32356163303536372d373034362d383633642d643834652d6537666630383935346238612e706e67.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1144/3fc72a4b-752b-41f8-8094-0d709ce97882.png">


### 3.3. コード修正
これに対応して以下のようにコード修正を行う

```diff:PageViewController.swift
--- a/PracticeApp/PageViewController.swift
+++ b/PracticeApp/PageViewController.swift
@@ -9,25 +9,23 @@
 import UIKit
 
 class PageViewController:
-    UIViewController,
+    UIPageViewController,
     UIPageViewControllerDelegate,
     UIPageViewControllerDataSource
 {
     var pages: [PageChildViewController] = [
-        PageChildViewController(pageIndex: 0),
+        PageChildViewController.getInstance(pageIndex: 0),
     ]
 
     override func viewDidLoad() {
-        let pc: UIPageViewController = UIPageViewController(transitionStyle: .scroll,
-                                                            navigationOrientation: .horizontal)
-        pc.dataSource = self
-        pc.delegate = self
-        pc.setViewControllers([self.pages[0]],
-                              direction: .forward,
-                              animated: false,
-                              completion: nil)
-        self.addChild(pc)
-        self.view.addSubview(pc.view)
+        super.viewDidLoad()
+
+        delegate = self
+        dataSource = self
+        setViewControllers([self.pages[0]],
+                           direction: .forward,
+                           animated: false,
+                           completion: nil)
     }
 
     // UIPageViewDataSource
@@ -59,7 +57,7 @@ class PageViewController:
             return nil
         }
         while self.pages.count <= pageIndex {
-            let vc: PageChildViewController = PageChildViewController(pageIndex: self.pages.count)
+            let vc = PageChildViewController.getInstance(pageIndex: self.pages.count)
             self.pages.append(vc)
         }
         return self.pages[pageIndex]
@@ -67,19 +65,19 @@ class PageViewController:
 }
 
 class PageChildViewController: UIViewController {
-    var pageIndex: Int = 0
-
-    required init?(coder aDecoder: NSCoder) {
-        fatalError("not implemented")
-    }
+    @IBOutlet weak var pageIndexLabel: UILabel!
+    @IBOutlet weak var redValueLabel: UILabel!
+    @IBOutlet weak var greenValueLabel: UILabel!
+    @IBOutlet weak var blueValueLabel: UILabel!
 
-    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
-        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
-    }
+    var pageIndex: Int = 0
 
-    convenience init(pageIndex: Int) {
-        self.init(nibName: nil, bundle: nil)
-        self.pageIndex = pageIndex
+    static func getInstance(pageIndex: Int) -> PageChildViewController
+    {
+        let storyboard: UIStoryboard = UIStoryboard(name:"Main", bundle:Bundle.main)
+        let child: PageChildViewController = storyboard.instantiateViewController(withIdentifier: "PageChildViewController") as! PageChildViewController
+        child.pageIndex = pageIndex
+        return child
     }
 
     override func viewDidLoad() {
@@ -101,10 +99,9 @@ class PageChildViewController: UIViewController {
         self.view.backgroundColor = UIColor(red: r, green: g, blue: b, alpha: 1)
 
         // label
-        let label: UILabel = UILabel(frame: view.bounds)
-        label.text = "page:\(pageIndex)"
-        label.textAlignment = .center
-
-        view.addSubview(label)
+        pageIndexLabel.text = "page:\(pageIndex)"
+        redValueLabel.text = "R:\(r)"
+        greenValueLabel.text = "G:\(g)"
+        blueValueLabel.text = "B:\(b)"
     }
 }
```
```diff:AppDelegate.swift
--- a/PracticeApp/AppDelegate.swift
+++ b/PracticeApp/AppDelegate.swift
@@ -16,23 +16,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
 
     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
         // Override point for customization after application launch.
-
-        let tabc: UITabBarController = window?.rootViewController as! UITabBarController
-        var newVcs: [UIViewController] = []
-
-        // tab1: navigation controller
-        for vc in tabc.viewControllers! {
-            if vc is UINavigationController {
-                newVcs.append(vc)
-            }
-        }
-
-        // tab2: pageview controller
-        let pageVc: PageViewController = PageViewController()
-        pageVc.tabBarItem = UITabBarItem(tabBarSystemItem: .bookmarks, tag: 1)
-        newVcs.append(pageVc)
-
-        tabc.setViewControllers(newVcs, animated: true)
         return true
     }
```

### 3.4. Storyboardの追加修正
PageViewControllerがUIPageViewControllerを継承したことにより、Storyboardのclass設定を追随できるようになっているはず
<img width="1543" alt="68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f313134342f34363364326363312d303161332d633266392d383566662d6363663361316463306664352e706e67.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1144/bb8abb4f-b988-ad94-4f36-ca009f70edfc.png">



### 3.5. 起動
Runする
<img width="510" alt="スクリーンショット 2019-03-03 23.22.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/1fe089ad-920b-5370-5357-d9a013cb42b9.png">


## 4. まとめ
最終的にPageViewControllerは下記のようになる

```swift:PageViewController.swift
import UIKit

class PageViewController:
    UIPageViewController,
    UIPageViewControllerDelegate,
    UIPageViewControllerDataSource
{
    var pages: [PageChildViewController] = [
        PageChildViewController.getInstance(pageIndex: 0),
    ]

    override func viewDidLoad() {
        super.viewDidLoad()

        delegate = self
        dataSource = self
        setViewControllers([self.pages[0]],
                           direction: .forward,
                           animated: false,
                           completion: nil)
    }

    // UIPageViewDataSource

    func pageViewController(_ pageViewController: UIPageViewController,
        viewControllerBefore viewController: UIViewController) -> UIViewController?
    {
        let child: PageChildViewController = viewController as! PageChildViewController
        let beforePageIndex: Int = child.pageIndex
        if 0 == beforePageIndex {
            return nil
        }
        return getPage(pageIndex: beforePageIndex - 1)
    }

    func pageViewController(_ pageViewController: UIPageViewController,
        viewControllerAfter viewController: UIViewController) -> UIViewController?
    {
        let child: PageChildViewController = viewController as! PageChildViewController
        let afterPageIndex: Int = child.pageIndex
        return getPage(pageIndex: afterPageIndex + 1)
    }

    // private

    func getPage(pageIndex: Int) -> PageChildViewController?
    {
        if pageIndex < 0 {
            return nil
        }
        while self.pages.count <= pageIndex {
            let vc = PageChildViewController.getInstance(pageIndex: self.pages.count)
            self.pages.append(vc)
        }
        return self.pages[pageIndex]
    }
}

class PageChildViewController: UIViewController {
    @IBOutlet weak var pageIndexLabel: UILabel!
    @IBOutlet weak var redValueLabel: UILabel!
    @IBOutlet weak var greenValueLabel: UILabel!
    @IBOutlet weak var blueValueLabel: UILabel!

    var pageIndex: Int = 0

    static func getInstance(pageIndex: Int) -> PageChildViewController
    {
        let storyboard: UIStoryboard = UIStoryboard(name:"Main", bundle:Bundle.main)
        let child: PageChildViewController = storyboard.instantiateViewController(withIdentifier: "PageChildViewController") as! PageChildViewController
        child.pageIndex = pageIndex
        return child
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // background color
        var r = 0.1 * CGFloat(self.pageIndex)
        while 1 < r {
            r -= 1
        }
        var g = 0.2 * CGFloat(self.pageIndex)
        while 1 < g {
            g -= 1
        }
        var b = 0.3 * CGFloat(self.pageIndex)
        while 1 < b {
            b -= 1
        }
        self.view.backgroundColor = UIColor(red: r, green: g, blue: b, alpha: 1)

        // label
        pageIndexLabel.text = "page:\(pageIndex)"
        redValueLabel.text = "R:\(r)"
        greenValueLabel.text = "G:\(g)"
        blueValueLabel.text = "B:\(b)"
    }
}
```

まとめになっていないが特に気にしない。