2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-04-16

目的

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

Step1. コードのみでUIPageViewController管理化

前回のコード をベースに作業。
今回もなるべく爆速で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を表示するようなイメージで以下。

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の設定は済んでいる前提。

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が組み合わさった適当アプリが動く。
スクリーンショット 2019-03-03 18.15.09.png

Step2. 動的paging+UIちょっと工夫

スワイプする分だけ(理論上)無限にpageが変わるように変更する。
また、ちょっと見た目もわかりやすく背景色をpage数によって変えてみる。

2.1. PageViewControllerの修正

以下のように修正

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すると、スワイプに応じて背景色の変わるアプリが動く
スクリーンショット 2019-03-03 18.32.06.png

Step3. Storyboardを使ってみる

これまで通り、今記述した部分をなるべくStoryboardにお任せする

3.1. UIPageViewControllerをStoryboardへ移行

まず不要なUIViewControllerを削除
スクリーンショット_2019-03-03_22_55_10.png

次にUIPageViewControllerを追加
スクリーンショット 2019-03-03 22.55.39.png

TabBarControllerのoutletsの「view controllers」とUIPageViewControllerをバインドする
スクリーンショット_2019-03-03_22_56_03.png

tabBarItemのSystem Itemsも設定しておく
スクリーンショット_2019-03-03_22_57_29.png

PageViewControllerの諸々の属性も設定しておく
スクリーンショット_2019-03-03_22_56_50.png

3.2. 子ページコントローラ(PageChildViewController)をStoryboardへ

新規にUIViewControllerを追加し、UILabelなどを配置
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f313134342f32393239333961612d666237322d613335632d343564642d3833316161303131363230342e706e67.png

IBOutletで各Labelをバインドするほか、クラス設定、Storyboard IDも忘れずに設定しておく
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f313134342f32356163303536372d373034362d383633642d643834652d6537666630383935346238612e706e67.png

3.3. コード修正

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

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)"
     }
 }
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設定を追随できるようになっているはず
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f313134342f34363364326363312d303161332d633266392d383566662d6363663361316463306664352e706e67.png

3.5. 起動

Runする
スクリーンショット 2019-03-03 23.22.21.png

4. まとめ

最終的にPageViewControllerは下記のようになる

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

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

2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?