iOS
Swift

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


目的


  • よく使うであろう(?)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)"
}
}


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