UIPageViewController 八つの掟

# UIPageViewController 八つの掟

結構理解しているつもりだった `UIPageViewController` でしたが、毎回同じようなところで何度もハマるので、忘れる前に記憶の整理を兼ねてメモしておく事とします。

![screenshot.jpg](https://qiita-image-store.s3.amazonaws.com/0/65634/d05bcf74-a83d-69b3-83fe-9bc898d7d865.jpeg)


## 一、PageCurl と Scroll を動的に変更したい場合は、InterfaceBuilderではなく、コードで作成すべし

`transitionStyle` は readonly なので、InterfaceBuilder でインスタンス化させると、`transitionStyle` は変更できません。それで `PageCurl` と `Scroll` を動的に変更したい場合は、自分の ViewController 内に、コードでインスタンス化させて追加します。そうすれば、変更したい場合は、`UIPageViewController` 自体を一旦削除して作り直す事ができます。

```.swift
var transitionStyle: UIPageViewControllerTransitionStyle { get }
```

> `transitionStyle` Property
> The style used to transition between view controllers. (read-only)


（追記）`self.addChildViewController(pageViewController)` も忘れないでください。

```.swift
pageViewController.willMoveToParentViewController(self)
self.addChildViewController(pageViewController)
pageViewController.setViewControllers(...)
pageViewController.view.frame = self.view.bounds
self.view.addSubview(pageViewController.view)
pageViewController.didMoveToParentViewController(self)
```

## 一、PageCurl で見開き対応したい時 UIPageController の viewControllers と　spineLocation を気をつけるべし

`UIPageViewController` の `transitionStyle` を `PageCurl` にした時は、`UIPageController` の `delegate` が呼ばれて、`pageViewController:spineLocationForInterfaceOrientation:` が呼ばれます。`spineLocation` は `.None`, `.Min`, `.Mid`, `.Max` とありますが、その組み合わせは以下の通りです。

(Landscapeの場合)

|ページ|右左綴じ|Spine|PageCurl|Scroll|
|:----|:-----|:----|:---|:---|
|単ページ表示|左綴じ（横書き）|.Min|1|1|
|単ページ表示|右綴じ（縦書き）|.Max|1|1|
|見開き表示|どちらでも|.Mid|2|1|

`Scroll` モード見開き表示の場合は、実質２ページであるにもかかわらず、`viewControllers` には１つの `viewController` しか配置できません。そこで、見開き用の `viewController` を用意して、その中に２つの `viewController` を配置する必要があります。

<pre>
	+ UIViewController （見開き用）
		- UIViewController　（ページ用）
		- UIViewController　（ページ用）
</pre>

## 一、PageCurlモードで、単ページ表示および見開きページの場合、右綴じは順序を逆にすべし

書籍などのコンテンツで、全ページ分 ViewController を準備する場合、右綴じの場合はその順序を逆に用意する必要があります。

<pre>
[1] [2] [3] [4] [5] [6]	（左綴じ）
[6] [5] [4] [3] [2] [1]	（右綴じ）

[1|2] [3|4] [5|6]	（左綴じ）
[6|5] [4|3] [2|1]	（右綴じ）
</pre>

もしくは、`pageViewController:viewControllerBeforeViewController:` または `pageViewController:viewControllerAfterViewController:` の`delegate` で右綴じ左綴じに応じて順方法または逆方法に `viewController` にアクセスしてやる必要があります。

<pre>
pageViewController:viewControllerAfterViewController:
</pre>

## 一、PageCurlモードで、見開きページの場合の場合、最初と最後のページも viewController を２つ用意するべし

多くの書籍は表紙、裏表紙を含めて偶数ページですが、その場合、表紙 ViewController と対となるダミー ViewController を用意しなくてはならなりません。

<pre>
[x|1] [2|3] [4|5] [6|x]		（x はダミーページ）
</pre>

## 一、単ページ表示の時、ダミーページを表示させるべからず

先のダミーページを含む見開き表示の場合、デバイスを回転させて単ページ表示になった時にダミーページを表示させてはいけません。わざとそうしたい場合はその限りではありません。

<pre>
（横）	（縦）
[x|1] -> [1]
[6|x] -> [6]
</pre>

## 一、PageCurl モードは pageViewController:spineLocationForInterfaceOrientation: で調整すべし

iOS8 より `interfaceOrientation` が Deprecated になりました。`Landscape` と `Portrait` の判定には、実際の `view` の縦と横のサイズを使うなど、別の方法が要求されるようになりました。しかし、この方法を使うと落とし穴もあります。`pageViewController:spineLocationForInterfaceOrientation:` が呼ばれた時にまだ、`view.bounds` が反映されていない時があるからです。

`interfaceOrientation` が Deprecated なのに、`pageViewController:spineLocationForInterfaceOrientation:` が `interfaceOrientation` を戻してくるのはいい度胸をしています。とにかく、`pageViewController:spineLocationForInterfaceOrientation:` が呼ばれた時、先ほどの表を元に、正しい `spineLocation` と 正しい数と内容の `viewController` をセットしましょう。

## 一、Scroll モードは viewWillLayoutSubviews で、UIPageViewControllerの調整をすべし

`Scroll` モードの場合は、`pageViewController:spineLocationForInterfaceOrientation:` は呼ばれません、`pageViewController:spineLocationForInterfaceOrientation:` 内で全て調整していると、`Scroll` モードの時に、デバイスを回転させても、`UIPageViewController` は素知らぬ顔です。

`Scroll` モードの場合は、`viewWillLayoutSubviews` などで `UIPageViewController` の `view` のサイズを確認して、`Landscape` または `Portrait` の判定を行い、単ページ・複数ページ表示の切り替えを行います。

そんなの PageCurlモードもこちらで調整すればいいと思うかもしれませんが、デバイスを回転させた時は`pageViewController:spineLocationForInterfaceOrientation:` が呼ばれ、正しい解答を答えないと例外が発生されてしまいます。`spineLocation` が `readonly` なので、この時に今後の予定の状態を正しくセットしておかないと、後で設定はできないのです。

## 一、Scroll モード、見開き、右綴じの場合、ページの位置を逆にすべし

`Scroll` モードの時の見開きページの場合に、見開き用ViewControllerを使う場合に、右綴じの場合、ページの配置を

<pre>
[x|6] [5|4] [3|2] [1|x]	-> OK
[6|x] [4|5] [2|3] [ |1]	-> NG
</pre>

# おまけ、interfaceOrientation に頼らず、縦横を判定する方法

#### 1. ViewController に オプショナルな `CGSize?` を用意する

```.swift
	var viewTransitioningSize: CGSize?
```

#### 2. viewWillTransitionToSize: で直近で設定されるサイズを保持

```.swift
	override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
		self.viewTransitioningSize = size // <-- before super
		super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
		// your code
	}
```

#### 3. viewWillLayoutSubviews で保持しているサイズを廃棄

```.swift
	override func viewWillLayoutSubviews() {
		self.viewTransitioningSize = nil
		super.viewWillLayoutSubviews()
	}
```

#### 4. Landscape Portrait を判定するメソッドを用意する

```.swift
	var isPortrait: Bool {
		assert(self.isViewLoaded())
		let viewSize = viewTransitioningSize ?? self.view.bounds.size
		return viewSize.height > viewSize.width
	}
```

これで、`pageViewController:spineLocationForInterfaceOrientation:` が呼ばれている最中で、`view.bounds` が確定していない時でも、`Landscape` と `Portrait` の判定が可能になります。


----
（参考資料）
UIPageViewController class reference - Apple Developer
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIPageViewControllerClassReferenceClassRef/