Adaptive UI Designについてのまとめ その2（Trait Enviornment, UISplitViewController, Presentation Controller, Appearance Proxy)

このドキュメントは、StoryboardからUISplitViewcontrollerを使ったサンプルの作成方法について説明します。

# 前提知識
## Adaptive UI Design
解像度や画面サイズの異なるデバイスに対応するため、iOS8以降はAdaptive User Interfaceという考え方が取り入れられました。

Size ClassesやTrait Collectionについての説明は、[こちらの記事](http://qiita.com/nomadmonad/items/1d7667dc0b0a5b3ee4ee)を参照してください。

## Trait Enviornments
iOS8から導入されたTrait Collectionは、実行するデバイスやデバイスの向きに応じて変化します。
変更されたTrait Collectionは、どれか1つの```UIViewController```にだけ適用されるわけでなく、アプリケーション全体（```UIScreen```から```UIView```や```UIPresentationController```まで）に適用されます。

![adaptive.jpg](https://qiita-image-store.s3.amazonaws.com/0/30970/2e353862-4497-3dcb-6256-26a555d57675.jpeg)

## UISplitViewController
上記のTrait Collectionに応じて表示される内容が変わる例として、```UISplitViewController```があります。
これまではiPadでのみ使えていましたが、iOS8以降、iPhoneでも使えるように変わりました。

特に何も手を加えず、Universal AppとしてStoryboardで作成した場合、Trait Collectionに応じて表示される内容が以下のように変わります。

- Horizontal Size ClassがRegular(iPad, iPhone6 Plus Landscape): Master/Detailの両方が表示される
- Horizontal Size ClassがCompact(iPhone6 Plus Landscape以外のiPhone): Master/Detailのどちらかのみ表示される

## Presentation Controller
Presentation Contrllerも同様に、Trait Collectionに応じて表示する内容が変わります。
iOS8からiPhoneでも使えるようになった、Popoverをサンプルに説明します。

### PopoverとTrait Collection
PopoverもTrait Collectionに応じて、表示され方が変わります。

- Horizontal Size ClassがRegular(iPad, iPhone6 Plus Landscape): Popoverで表示される
- Horizontal Size ClassがCompact(iPhone6 Plus Landscape以外のiPhone): FullScreenで表示される

UIKitの中で、Trait Collectionに応じて最適な表示方法を実現するため、上記のように切り替えていると思われます。

### Adaptive Presentation Controller
Trait Collectionに応じた描画内容の変更を、開発者が自分で調整したいという場合は、```UIAdaptivePresentationControllerDelegate```を使ってハンドリングすることもできます。

例えば、"Popover Segueを使い、Horizontal Size ClassがCompactの場合でもPopoverで表示したい"というような場合、以下のように実装すると必ずPopoverで表示することができます。

```objc
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier compare:@"showPopOver"] == NSOrderedSame) {
        UINavigationController * nvc = segue.destinationViewController;
        UIPresentationController * pc = nvc.presentationController;
        pc.delegate = self;
        // In WWDC 2014 "View Controller Advancements in iOS8",
        // below codes can show popover on iPhone (Adaptive popover).
        //
        // UINavigationController * nvc = segue.destinationViewController;
        // UIPopoverPresentationController * pvc = nvc.popoverPresentationController;
        // pvc.delegate = self;
        //
        // But On Xcode 6.1, these codes shows FullScreen presentation...
        // (nvc.popoverPresentationController is nil)
        // For workarounds, I use presentationController's delegate instead of popoverController's.
    }
}

#pragma mark == UIPopoverPresentationControllerDelegate ==

- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
{
    return UIModalPresentationNone;
}

```
iPhoneでPopoverを表示する例: 
![popover.jpg](https://qiita-image-store.s3.amazonaws.com/0/30970/e597919f-35eb-4471-70ee-eb4b85e48341.jpeg)

**上記の例は、Xcode6.1で動作確認できたものです。以降のバージョンでは使えなくなる可能性がありますので、実装時には確認したうえで試してみてください。**

## Appearance Proxy
iOS5から追加された```UIAppearance```にも、Trait Collectionに対応したメソッドが追加されています。
以下の例は、```apperanceForTraitCollection```を使ってUIButtonの背景画像を変えるサンプルです。

```objc:デバイスの向きに応じてUIButtonの背景画像を変えるサンプル
// iOS7.x以前のサンプル
// iOS7.x以前なら、デバイスの向きが変更されたタイミングで切り替える処理が必要だった。
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
    UIButton btn;
    if (UIInterfaceOrientationIsPortrait(orientation)) {
        [btn setBackgroundImage:[UIImage imageNamed:@"image1"]
                     forBarMetrics:UIBarMetricsDefault];
    } else {
        [btn setBackgroundImage:[UIImage imageNamed:@"image2"]
                     forBarMetrics:UIBarMetricsDefault];
    }
}
```

```objc:TraitCollectionに応じてUIButtonの背景画像を変えるサンプル
// iOS8以降なら、Appearance Proxyを使って以下のように設定できる。
// AppDelegateあたりに書いておけばアプリケーション全体で使いまわせる。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    [[UIButton appearance] setBackgroundImage:[UIImage imageNamed:@"image1"]
                                forBarMetrics:UIBarMetricsDefault];
    [[UIButton appearanceForTraitCollection:
      [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassCompact]] 
         setBackgroundImage:[UIImage imageNamed:@"image2"] forBarMetrics:UIBarMetricsDefault];
}
```

# UISplitViewControllerを使ったサンプル
ここまでの内容を元に、```UISplitViewController```を使ったサンプルを作成してみます。
実現したいこととしては、

- どのデバイスでも、UISplitViewControllerを使って画面を表示させたい
- どのデバイスでもPopoverで何かしら表示させたい

ソースコードは[こちら](https://github.com/nomadmonad/SplitViewControllerSample)のものを確認してください。

## プロジェクトの作成
はじめに、Universal Device向けのプロジェクトを作成します。
名前は何でも構いません。

## Storyboardの作成
Storyboardで、以下のようにView ControllerやUIViewを配置し、segueで接続します。
![storyboard.jpg](https://qiita-image-store.s3.amazonaws.com/0/30970/cd9ede53-1598-d48f-3edf-7897dfc07387.jpeg)


## UISplitViewContollerをchildViewControllerとして追加・delegateの設定

上記サンプル中の```ViewController```で、```UISplitViewControllerDelegate```を適用した上で以下の通り実装します。

```objc:ViewController.m
@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    // 省略
    UISplitViewController * svc = [self.childViewControllers firstObject];
    // Primary側のViewControllerの表示モードを指定する。
    // 今回は両方共表示するように指定。
    svc.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible;
    
    svc.preferredPrimaryColumnWidthFraction = 0.5;
    svc.maximumPrimaryColumnWidth = 500;
}

- (void)addChildViewController:(UIViewController *)childController
{
    // このサンプルでは、ChildViewControllerを設定するときにUISplitViewConrollerDelegateを指定しておく
    [super addChildViewController:childController];
    UISplitViewController * svc = [self.childViewControllers firstObject];

    if (svc) {
        svc.delegate = self;
    }
}

#pragma mark == UISplitViewControllerDelegate ==

- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController
{
    // Primary側（Master）の方を表示させておきたいので、
    // Secondary(Detail)はcollapseで表示するよう指定する。
    return YES;
}

@end
```
あとはMaster/Detailで表示するViewControllerの実装ができていれば、問題なく表示できているはずです。

## Segueを使ってどのデバイスでもPopoverで表示させる
Segueを使ってどのデバイスでもPopoverで表示させたい場合は、上記[Adaptive Presentation Controller]で説明したように

* ```prepareForSegue:```で```self.presentationController```のdelegateを設定する
* ```adaptivePresentationStyleForPresentationController:```で```UIPresentationStyleNone```を戻り値として返すよう指定する

の通り実装すればPopoverで表示されます。

## 気になった点
### SplitViewControllerからMaster/DetailのSegueを設定する時、どちら側を指定しようとしているのかわかりづらい
SplitViewControllerにMaster/DetailのSegueを設定する際、何も表示されないので非常にわかりづらいです。
Xcode6.2以降では改善されていることを期待しています。

### 他のSegueと比べて実装しなければならないことが多い
showやpresent modally等の他のSegueに比べ、コード側で指定しなければならない項目が多いように感じています。
Storyboardとコードから指定しなければならない項目が分散してしまっているので、なんとなく据わりの悪い印象を受けています。

Storyboardで指定しておくほうが、今後のiOSのバージョンアップを考えると良いのかもしれませんが………コード側で実装してしまうほうが早いよなあとも感じています。

# 参考にしたURL
## WWDC 2014

[What's New in Cocoa Touch](http://devstreaming.apple.com/videos/wwdc/2014/202xx3ane09vxdz/202/202_whats_new_in_cocoa_touch.pdf?dl=1)
[Building Adaptive Apps with UIKit](http://devstreaming.apple.com/videos/wwdc/2014/216xxcnxc6wnkf3/216/216_building_adaptive_apps_with_uikit.pdf?dl=1)
[What's New in Interface Builder](http://devstreaming.apple.com/videos/wwdc/2014/411xx0xo98zzoor/411/411_whats_new_in_interface_builder.pdf?dl=1)

## Apple Developer Library
[What's New In iOS: iOS8](https://developer.apple.com/library/ios/releasenotes/General/WhatsNewIniOS/Articles/iOS8.html)
[iOS Human Interface Guidelines Adaptivity and Layout](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/LayoutandAppearance.html)
[Deploying an App With Size Classes on Earlier iOS Versions](https://developer.apple.com/library/ios/recipes/xcode_help-IB_adaptive_sizes/chapters/DeployingSizeClassesonEarlieriOSVersions.html#//apple_ref/doc/uid/TP40014436-CH13-SW1)
[AdaptivePhotos: An Adaptive Application](https://developer.apple.com/library/prerelease/ios/samplecode/AdaptivePhotos/Introduction/Intro.html)

## Others
[iMore: Adaptive UI in iOS 8: Explained](http://www.imore.com/adaptive-ui-ios-8-explained)
[Beginning Adaptive Layout Tutorial](http://www.raywenderlich.com/83276/beginning-adaptive-layout-tutorial)
[Nerdely: Overcoming the iPad Tax With iOS 8 and Adaptive UI](http://blog.nerdery.com/2014/08/overcoming-ipad-tax-ios-8-adaptive-ui/)
[Xamarin: Introduction to Unified Storyboards](http://developer.xamarin.com/guides/ios/platform_features/intro_to_unified_Storyboards/)
[stackoverflow: Design app that supports both iOS8 and iOS7](http://stackoverflow.com/questions/26293829/design-app-that-supports-both-ios8-and-ios7)
[stackoverflow: Xcode6 Universal Storyboards and view sizing](http://stackoverflow.com/questions/24377035/xcode6-universal-storyboards-and-view-sizing)
[stackoverflow: Xcode 6 – Unified storyboard & Size Classes](http://stackoverflow.com/questions/24075597/xcode-6-unified-storyboard-size-classes)
[stackoverflow: How can Xcode 6 adaptive UIs be backwards-compatible with iOS 7 and iOS 6?](http://stackoverflow.com/questions/24172860/how-can-xcode-6-adaptive-uis-be-backwards-compatible-with-ios-7-and-ios-6)
[stackoverflow: Example for iPhone portrait landscape adaptive UI which is backwards-compatible with iOS 7](http://stackoverflow.com/questions/26694627/example-for-iphone-portrait-landscape-adaptive-ui-which-is-backwards-compatible)])