次の方法で共有


Xamarin.iOS の統合ストーリーボード

iOS 8 には、統合ストーリーボードという使いやすいユーザー インターフェイス作成メカニズムが新たに用意されています。 1 つのストーリーボードでさまざまなハードウェア画面サイズに対応できるため、高速で応答性に優れたビューを "一度デザインして繰り返し利用する" スタイルで作成できます。

iPhone デバイスと iPad デバイスのために異なるストーリーボードを作成する必要がないため、開発者は、まず 1 つの共通インターフェイスでアプリケーションを設計してから、そのインターフェイスをさまざまなサイズ クラス用にカスタマイズすることができます。 アプリケーションをそれぞれのフォーム ファクターに適合させてその環境の長所を生かし、個々のユーザー インターフェイスを調整して最良のエクスペリエンスを提供できます。

サイズ クラス

iOS 8 より前は、開発者が UIInterfaceOrientationUIInterfaceIdiom を使用してポートレート モードとランドスケープ モードの区別、iPhone デバイスと iPad デバイスの区別を付けていました。 iOS8 では、画面の向きとデバイスは "サイズ クラス" を使用して決定されます。

デバイスはサイズ クラスによって (縦軸、横軸の両方について) 定義され、iOS 8 の場合は以下 2 種類のサイズ クラスがあります。

  • Regular – 大型の画面サイズ (iPad など)、または大きいサイズの印象を与えるガジェット用 (UIScrollView など)。
  • Compact – 小型デバイス用 (iPhone など)。 このサイズでは、デバイスの向きが考慮されます。

2 つの概念を組み合わせると、次の図に示すように、画面の向きの違いを含めたサイズのバリエーションを 2 x 2 のグリッドで表すことができます。

標準方向とコンパクト方向で使用できるさまざまなサイズを定義する 2 x 2 グリッド

開発者は、4 とおりのレイアウトで表示される可能性があるビュー コントローラーを作成できます (上の図を参照)。

iPad のサイズ クラス

iPad はサイズが大きいため、どちらの向きも Regular サイズ クラスに該当します。

iPad のサイズ クラス

iPhone のサイズ クラス

iPhone のサイズ クラスは、デバイスの向きによって変化します。

iPhone のサイズ クラス

  • デバイスが縦向きモードのとき、画面の水平方向は Compact クラス、垂直方向は Regular クラスです。
  • デバイスが横向きモードのとき、画面のサイズ クラスは縦向きモードの場合とは反対になります。

iPhone 6 Plus のサイズ クラス

デバイスが縦向きのときは以前の iPhone と同じサイズ クラスですが、横向きのときは異なります。

iPhone 6 Plus のサイズ クラス

iPhone 6 Plus は画面が十分に大きいため、横向きモードでは水平方向のサイズ クラスが Regular の範疇に入ります。

新しい画面スケールのサポート

iPhone 6 Plus には、画面スケール ファクターが 3.0 (初代 iPhone の画面解像度の 3 倍) となる新しい Retina HD ディスプレイが採用されています。 このデバイスで実現可能な最良のエクスペリエンスを提供するには、この画面スケール用にデザインした新しいアートワークを含めてください。 Xcode 6 以降では、アセット カタログに 1x、2x、3x サイズの画像を含めることができます。新しい画像アセットを追加するだけで、iPhone 6 Plus で実行すると iOS によって正しいアセットが選択されます。

iOS の画像読み込み動作では、画像ファイルの @3x サフィックスも認識されます。 たとえば、開発者はアプリケーションのバンドルに MonkeyIcon.pngMonkeyIcon@2x.pngMonkeyIcon@3x.png というファイル名の画像アセットを (それぞれのスケールに適した解像度で) 含めることができます。 iPhone 6 Plus では、次のコードを使用して画像をロードすると MonkeyIcon@3x.png ファイルの画像が自動的に選択されます。

UIImage icon = UIImage.FromFile("MonkeyImage.png");

動的起動画面

起動画面ファイルは、iOS アプリケーションの起動時にスプラッシュ画面として表示され、アプリの起動処理が実際に進んでいることをユーザーに知らせます。 iOS 8 より前のバージョンでは、開発者が、アプリケーションを実行するデバイスの種類、画面の向き、解像度ごとに複数の Default.png 画像アセットを用意する必要がありました。

iOS 8 では、開発者が Xcode で単一のアトミックな .xib ファイルを作成できるようになりました。このファイルから自動レイアウト クラスとサイズ クラスに基づいて 動的起動画面が作成されるため、すべてのデバイス、解像度、向きに対応できます。 これには、必要とされるすべての画像アセットを作成し保守する開発者の作業負荷が軽減されるのに加え、インストールされるアプリケーション バンドルのサイズも抑えられるメリットがあります。

Traits

トレイト (形質) は、環境の変化に応じたレイアウト変化の方法を指定するために使用できるプロパティ群です。 一連のプロパティ (UIUserInterfaceSizeClass に基づく HorizontalSizeClassVerticalSizeClass) と、インターフェイス イディオム (UIUserInterfaceIdiom)、および表示スケールで構成されます。

これら各種の状態は、Apple の用語でトレイト コレクションと呼ばれるコンテナー (UITraitCollection) の中に、プロパティと値を含めすべてラッピングされます。

トレイト環境

トレイト環境は iOS 8 で新設されたインターフェイスであり、以下のオブジェクトに関するトレイト コレクションを返す機能があります。

  • 画面 ( UIScreens )。
  • ウィンドウ ( UIWindows )。
  • ビュー コントローラー ( UIViewController )。
  • ビュー ( UIView )。
  • プレゼンテーション コントローラー ( UIPresentationController )。

開発者は、トレイト環境から返されるトレイト コレクションに基づいてユーザー インターフェイスのレイアウト方法を決定できます。

各種トレイト環境の間には、次の図に示す階層構造の関係があります。

特性環境の階層図

上の図に含まれる各トレイト環境の中のトレイト コレクションは、既定では、親階層の環境から子階層の環境へと伝播していきます。

トレイト環境には、現在のトレイト コレクションを取得するメソッドに加え、ビューまたはビュー コントローラーのサブクラスでオーバーライド可能な TraitCollectionDidChange メソッドがあります。 開発者は、トレイトに変化が生じた場合にこのメソッドで変化を検知し、トレイトに依存する UI 要素に変更を加えることができます。

一般的なトレイト コレクション

このセクションでは、iOS 8 で開発者が扱う一般的なトレイト コレクションの種類について説明します。

開発者が iPhone で扱う可能性がある一般的なトレイト コレクションを以下に示します。

プロパティ
HorizontalSizeClass コンパクト
VerticalSizeClass Regular
UserInterfaceIdom 電話番号
DisplayScale 2.0

このセットは完全修飾トレイト コレクションです。つまり、これらのトレイト プロパティには必ず値が設定されています。

一方、以下のように、場合によって一部の値が未指定 (Apple の用語で "Unspecified") になるトレイト コレクションもあります。

プロパティ
HorizontalSizeClass コンパクト
VerticalSizeClass 指定されていません。
UserInterfaceIdom 指定されていません。
DisplayScale 指定されていません。

ただし、開発者がトレイト環境からトレイト コレクションを取得すると、多くの場合は、前の例のような完全修飾コレクションが返されます。

現在のビュー階層内に属していないトレイト環境 (たとえば、ビューやビュー コントローラー) の場合は、いくつかのトレイト プロパティが未指定として返される可能性があります。

また、Apple から提供された作成方法のいずれか (UITraitCollection.FromHorizontalSizeClass など) によって新しいコレクションを作成する場合にも、一部の値が未設定になっているトレイト コレクションが得られます。

複数のトレイト コレクションを対象にすることができる操作として、コレクション間の比較操作があります。これは、第 1 のトレイト コレクションに第 2 のトレイト コレクションが包含されているかどうかをチェックするものです。 ここでいう "包含" とは、第 2 コレクション内で設定されているすべてのトレイトの値が、第 1 コレクション内の指定値と完全に一致するということです。

2 つのトレイトをテストするには、UITraitCollectionContains メソッドを、テスト対象のトレイト値を渡して呼び出します。

開発者は、自分のコードで比較を実行してビューやビュー コントローラーのレイアウト方法を決めることもできますが、 このメソッドは、外観プロキシなどいくつかの機能を実現するために UIKit の内部でも使用されています。

外観プロキシ

Appearance Proxy (外観プロキシ) は、以前の iOS バージョンで、ビューのプロパティを開発者がカスタマイズできるようにするために導入されました。 それが iOS 8 ではトレイト コレクションをサポートするように拡張されました。

Appearance Proxy に追加された新しいメソッド AppearanceForTraitCollection は、渡された特定のトレイト コレクションを対象とする新しい外観プロキシを返します。 その外観プロキシを開発者がカスタマイズすると、指定されたトレイト コレクションに準拠したビューのみに対してカスタマイズが反映されます。

通常、開発者は、値を部分的に指定したトレイト コレクションを AppearanceForTraitCollection メソッドに渡します。たとえば、水平方向のサイズ クラスとして Compact を指定しただけのトレイト コレクションを渡すと、アプリケーション内の、水平方向が Compact である任意のビューをカスタマイズできます。

UIImage

UIImage も、Apple によってトレイト コレクションへの対応が行われたクラスの 1 つです。 以前は、アプリケーションに含めるすべてのビットマップ グラフィック アセット (アイコンなど) について開発者が @1x と @2x の両バージョンを指定する必要がありました。

iOS 8 での機能拡張により、開発者はトレイト コレクションに基づく複数バージョンの画像を画像カタログに含められるようになりました。 たとえば、Compact トレイト クラス向けの小さい画像と、それ以外のコレクション向けのフル サイズ画像を両方とも用意することができます。

そうした画像のいずれかを UIImageView クラス内で使用する箇所では、トレイト コレクションから適切なバージョンの画像が自動的に選択されて画像ビューに表示されます。 トレイト環境が変化すると (デバイスが縦向きから横向きに切り替えられた場合など)、切り替え後のトレイト コレクションに合った新しい画像サイズが自動的に選択され、そのバージョンの画像サイズに合わせて画像ビューのサイズが変更されます。

UIImageAsset

Apple が iOS 8 に追加した新しいクラス UIImageAsset は、開発者にいっそう細かく画像選択を制御する手段を提供するものです。

画像アセットは 1 つの画像のさまざまなバージョンをまとめてラッピングするオブジェクトであり、開発者はその中から、渡されたトレイト コレクションに合う特定の画像を取得できます。 画像アセットに含められた画像は、動作中にいつでも追加または削除できます。

画像アセットの詳細については、Apple の UIImageAsset ドキュメントを参照してください。

トレイト コレクションの結合

トレイト コレクションについては、2 つのトレイト コレクションを加算して結合結果のコレクションを得る操作も実行できます。ある値が一方のコレクション内では未指定、もう一方のコレクション内では指定されている場合、結果には、指定がある後者の値が格納されます。 この操作を実行するには、UITraitCollection クラスの FromTraitsFromCollections メソッドを使用します。

前述のように、あるトレイトについて 1 つのトレイト コレクション内では未指定、もう 1 つのコレクション内では値が指定されている場合、指定があるバージョンの値が採用されます。 ただし、あるトレイトについて複数のコレクション内に値が指定されている場合は、最後のトレイト コレクションに含まれる値が採用されます。

適応型ビュー コントローラー

このセクションでは、iOS のビューとビュー コントローラーにトレイトとサイズ クラスの概念が導入され、開発者のアプリケーション内における動作の適応性が自動的に従来よりも高まったことについて詳しく説明します。

分割ビュー コントローラー

UISplitViewController クラスは、iOS 8 において非常に大きな変更が行われたビュー コントローラークラスの 1 つです。 以前は、分割ビュー コントローラーを使用して iPad 用のアプリケーションを作成した開発者が iPhone に対応する際、まったく異なるビュー階層構造を採用した別バージョンを提供しなくてはならない場合がよくありました。

iOS 8 では UISplitViewController クラスを両方のプラットフォーム (iPad と iPhone) で使用できるようになったため、1 つのビュー コントローラー階層を採用してアプリケーションを開発し、iPhone と iPad の両方で動作させることができます。

iPhone デバイスが横向きモードのとき、分割ビュー コントローラーは、iPad での表示と同じように 2 つのビューを左右に並べて提示します。

トレイトのオーバーライド

トレイト環境内では、親階層のコンテナーから子階層のコンテナーへとトレイトが伝播します。たとえば、横向きの iPad に表示された分割ビュー コントローラーのトレイト環境は次の図のようになっています。

横向きの iPad の分割ビュー コントローラー

iPad の場合は水平方向と垂直方向の両方が Regular サイズ クラスであるため、分割ビューにはマスター ビューと詳細ビューの両方が表示されます。

iPhone の場合は両方のサイズ クラスが Compact であるため、次のようになり、分割ビュー コントローラーには詳細ビューのみが表示されます。

分割ビュー コントローラーには詳細ビューのみが表示されます

iPhone でも横向きモード時にマスター ビューと詳細ビューの両方を表示するには、アプリケーション開発者が分割ビュー コントローラーの親コンテナーを挿入してトレイト コレクションをオーバーライドする必要があります。 これを図で示すと下のようになります。

開発者は、分割ビュー コントローラーの親コンテナーを挿入し、特性コレクションをオーバーライドする必要があります

分割ビュー コントローラーの親階層として UIView を設定し、これに対して、SetOverrideTraitCollection メソッド呼び出しで新しいトレイト コレクションを渡すと、子階層の分割ビュー コントローラーがターゲットになります。 新しいトレイト コレクションによって HorizontalSizeClass をオーバーライドし、Regular に設定することで、iPhone の横向きモードでも、分割ビュー コントローラーにマスター ビューと詳細ビューの両方が表示されるようになります。

親階層に渡す新しいトレイト コレクションの VerticalSizeClassunspecified になっている点にも注目してください。これにより、縦向きモードでは子階層の分割ビュー コントローラーに Compact VerticalSizeClass が適用されます。

トレイトの変化

このセクションでは、トレイト環境が変化したときトレイト コレクションに発生する遷移について詳しく説明します。 これは、たとえばデバイスの持ち方が縦向きから横向きに変わった場合などに起きる変化です。

横向きの縦向きの特性変更の概要

iOS 8 は、まず、遷移を行うための準備段階のセットアップを実行します。 次に、遷移状態のアニメーション処理を実行します。 最後に、遷移を進める間だけ必要になった一時的な状態をクリーンアップします。

iOS 8 には、開発者がトレイトの変化に関与する手段として、次の表に示すコールバックが用意されています。

段階 Callback 説明
段取り
  • WillTransitionToTraitCollection
  • TraitCollectionDidChange
  • このメソッドは、トレイトの変化が始まるとき、トレイト コレクションに新しい値が設定される前に呼び出されます。
  • このメソッドは、トレイト コレクションの値が変更された後、アニメーション処理が実行される前に呼び出されます。
アニメーション WillTransitionToTraitCollection このメソッドに渡される遷移コーディネーターの AnimateAlongside プロパティを使用すると、既定のアニメーションと並行して表示するアニメーションを開発者が追加できます。
クリーンアップ WillTransitionToTraitCollection 遷移の完了後に実行するクリーンアップ コードを開発者が独自に追加できます。

WillTransitionToTraitCollection メソッドは、トレイト コレクションが変化するのに合わせてビュー コントローラーのアニメーションを表示するのに最適です。 WillTransitionToTraitCollection メソッドはビュー コントローラー(UIViewController) でのみ使用でき、他のトレイト環境 (UIViews など) にはありません。

TraitCollectionDidChange は、UIView クラスで、トレイトの変化に合わせて UI を更新するのに最適です。

分割ビュー コントローラーの折りたたみ

次は、分割ビュー コントローラーが 2 列ビューから 1 列ビューに折りたたまれる際に起きることを詳しく説明します。 この変化においては、以下 2 つのプロセスが実行される必要があります。

  • 分割ビュー コントローラーが折りたたまれると、既定ではプライマリ ビュー コントローラーが表示されます。 開発者がこの動作を変更するには、UISplitViewControllerDelegateGetPrimaryViewControllerForCollapsingSplitViewController メソッドをオーバーライドして、折りたたみ状態でどのビュー コントローラーを表示するかを指定します。
  • セカンダリ ビュー コントローラーは、プライマリ ビュー コントローラーにマージされます。 通常、このステップに関して開発者が何かを行う必要はありません。分割ビュー コントローラーには、ハードウェア デバイスに応じた方法でこのフェーズを自動的に処理する機能が備わっています。 ただし、開発者がこの変化に関与することが必要になる特殊なケースもあります。 折りたたまれたときに詳細ビューではなくマスター ビュー コントローラーが表示されるようにするには、UISplitViewControllerDelegateCollapseSecondViewController メソッドを呼び出します。

分割ビュー コントローラーの展開

次は、分割ビュー コントローラーが折りたたみ状態から展開されたときに起きることを詳しく説明します。 折りたたまれる場合と同様に、以下 2 つの段階が実行される必要があります。

  • まず、新しいプライマリ ビュー コントローラーが定義されます。 このとき、既定では、折りたたみ状態のときに使用していたプライマリ ビュー コントローラーが自動的に使用されます。 開発者がこの動作をオーバーライドするには、前述の場合と同様に、UISplitViewControllerDelegateGetPrimaryViewControllerForExpandingSplitViewController メソッドを使用します。
  • プライマリ ビュー コントローラーの選択に続いて、セカンダリ ビュー コントローラーが再作成されます。 前述の場合と同様に、分割ビュー コントローラーには、ハードウェア デバイスに応じた方法でこのフェーズを自動的に処理する機能が備わっています。 開発者がこの動作を変更するには、UISplitViewControllerDelegateSeparateSecondaryViewController メソッドを呼び出します。

分割ビュー コントローラーのプライマリ ビュー コントローラーは、UISplitViewControllerDelegateCollapseSecondViewController メソッドと SeparateSecondaryViewController メソッドを実装することで、ビューの展開と折りたたみの両方に関与します。 UINavigationController では、これらのメソッドを実装することで、セカンダリ ビュー コントローラーのプッシュとポップを自動的に処理します。

ビュー コントローラーの表示

iOS 8 では、開発者がビュー コントローラーを表示する方法も Apple によって変更されています。 アプリケーション内にリーフ ビュー コントローラー(テーブル ビュー コントローラーなど) が含まれている場合に、異なるリーフ ビュー コントローラーを開発者が表示するとき (たとえば、ユーザーによるセルのタップ操作への応答)、以前は、コントローラー階層構造をたどって Navigation View Controller に戻り、それに対し PushViewController メソッドを呼び出すことによって新しいビューを表示する必要がありました。

その結果、ナビゲーション コントローラーと実行環境との間にきわめて密接な結びつきが生まれていました。 iOS 8 で、Apple は以下 2 つのメソッドを新設することにより、この結びつきを分離しました。

  • ShowViewController – 新しいビュー コントローラーを表示するための調整を環境に基づいて行います。 たとえば、UINavigationController では、単に新しいビューをスタックにプッシュするだけで済みます。 分割ビュー コントローラーでは、新しいビュー コントローラーは新しいプライマリ ビュー コントローラーとして左側に提示されます。 コンテナー ビュー コントローラーが存在しない場合、新しいビューはモーダル ビュー コントローラーとして表示されます。
  • ShowDetailViewControllerShowViewController に似た機能ですが、分割ビュー コントローラーに実装されるメソッドであり、詳細ビューを、渡された新しいビュー コントローラーで置き換えます。 分割ビュー コントローラーが折りたたまれている場合は (iPhone アプリケーションなど)、呼び出しが ShowViewController メソッドにリダイレクトされ、新しいビューはプライマリ ビュー コントローラーとして表示されます。 同様に、コンテナー ビュー コントローラーが存在しない場合、新しいビューはモーダル ビュー コントローラーとして表示されます。

これらのメソッドは、リーフ ビュー コントローラーを出発点として、新しいビューの表示を処理する適切なコンテナー ビュー コントローラーが見つかるまでビュー階層構造を上へたどっていくことにより機能します。

開発者は、独自にカスタマイズしたビュー コントローラー内に ShowViewControllerShowDetailViewController を実装することで、UINavigationController および UISplitViewController と同様の自動化された機能を実現できます。

しくみ

このセクションでは、これらのメソッドが iOS8 において実際どのように実装されているかを説明します。 まず、新しい GetTargetForAction メソッドから見ていくことにします。

新しい GetTargetForAction メソッド

このメソッドは、正しいコンテナー ビュー コントローラーが見つかるまで階層構造のチェーンをたどります。 次に例を示します。

  1. ShowViewController メソッドが呼び出された場合、このメソッドをチェーン内で実装している最初のビュー コントローラーはナビゲーション コントローラーであるため、これが新しいビューの親として使用されます。
  2. ShowDetailViewController メソッドが呼び出された場合、このメソッドをチェーン内で実装している最初のビュー コントローラーは分割ビュー コントローラーであるため、これが親として使用されます。

GetTargetForAction メソッドは、指定されたアクションを実装しているビュー コントローラーを探した後、そのアクションを受け付けるかどうかをビュー コントローラーに照会します。 これはパブリック メソッドであるため、開発者は、組み込みの ShowViewController メソッドや ShowDetailViewController メソッドと同様に機能する独自のカスタム メソッドを作成できます。

適応型プレゼンテーション

ポップオーバー プレゼンテーション (UIPopoverPresentationController) も、Apple の変更によって iOS 8 から適応型になりました。 ポップオーバー プレゼンテーション ビュー コントローラーは、Regular サイズ クラスでは自動的に通常のポップオーバー ビューを提示しますが、水平方向のサイズ クラスが Compact である場合 (iPhone など) は全画面表示になります。

統合ストーリーボード システム内の変更に対応するために、提示するビュー コントローラーの管理を行う新しいコントローラー オブジェクト、UIPresentationController が作成されました。 このコントローラーは、ビュー コントローラーが提示される時点て作成され、閉じられる時点まで存続します。 これは管理用クラスであり、ビュー コントローラーのスーパー クラスと見なすことができます。デバイスに起きる変化のうちビュー コントローラーに影響するもの (向きの変化など) に応答し、プレゼンテーション コントローラーの制御対象であるビュー コントローラーに変化をフィードバックします。

開発者が PresentViewController メソッドを使用してビュー コントローラーを提示すると、その提示プロセスの管理は UIKit に引き渡されます。 UIKit は、(さまざまな役割の 1 つとして) 作成するスタイルに応じた正しいコントローラーの処理を行います。ただし、ビュー コントローラーのスタイルが UIModalPresentationCustom に設定されている場合だけは例外です。 その場合、アプリケーションは、UIKit コントローラーを使用せずに独自の PresentationController を提供することができます。

カスタム プレゼンテーション スタイル

カスタム プレゼンテーション スタイルの場合、開発者はカスタム プレゼンテーション コントローラーを使用することを選択できます。 このカスタム コントローラーを使用すると、関連付けられたするビューの外観や動作を変更できます。

サイズ クラスの扱い方

この記事に含まれる Xamarin プロジェクト "Adaptive Photos" は、iOS 8 統一インターフェイス アプリケーションでサイズ クラスと適応型ビュー コントローラーを実際に使用する方法のサンプルです。

このアプリケーションの UI はすべてコーディングで実現されており、Xcode のインターフェイス ビルダーで統合ストーリーボードを作成する方法ではありませんが、適用されるテクニックは同じです。

Adaptive Photos プロジェクトで、数種類の iOS 8 サイズ クラス機能がどのように実装され、適応型アプリケーションが実現されているかを詳しく見てみましょう。

トレイト環境の変化への対応

Adaptive Photos アプリケーションを iPhone で実行した場合、デバイスが回転されて縦向きから横向きになると、分割ビュー コントローラーにマスター ビューと詳細ビューの両方が表示されます。

分割ビュー コントローラーには、次に示すようにマスター ビューと詳細ビューの両方が表示されます

これは、ビュー コントローラーの UpdateConstraintsForTraitCollection メソッドをオーバーライドし、VerticalSizeClass の値に基づいて制約を調整することにより実現されています。 次に例を示します。

public void UpdateConstraintsForTraitCollection (UITraitCollection collection)
{
    var views = NSDictionary.FromObjectsAndKeys (
        new object[] { TopLayoutGuide, ImageView, NameLabel, ConversationsLabel, PhotosLabel },
        new object[] { "topLayoutGuide", "imageView", "nameLabel", "conversationsLabel", "photosLabel" }
    );

    var newConstraints = new List<NSLayoutConstraint> ();
    if (collection.VerticalSizeClass == UIUserInterfaceSizeClass.Compact) {
        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|[imageView]-[nameLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("[imageView]-[conversationsLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("[imageView]-[photosLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("V:|[topLayoutGuide]-[nameLabel]-[conversationsLabel]-[photosLabel]",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("V:|[topLayoutGuide][imageView]|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.Add (NSLayoutConstraint.Create (ImageView, NSLayoutAttribute.Width, NSLayoutRelation.Equal,
            View, NSLayoutAttribute.Width, 0.5f, 0.0f));
    } else {
        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|[imageView]|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|-[nameLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|-[conversationsLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|-[photosLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("V:[topLayoutGuide]-[nameLabel]-[conversationsLabel]-[photosLabel]-20-[imageView]|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));
    }

    if (constraints != null)
        View.RemoveConstraints (constraints.ToArray ());

    constraints = newConstraints;
    View.AddConstraints (constraints.ToArray ());
}

遷移アニメーションの追加

Adaptive Photos アプリケーションでは、既定のアニメーションに加え、次のようにビュー コントローラーの WillTransitionToTraitCollection メソッドをオーバーライドすることで、分割ビュー コントローラーが折りたたみ状態から展開状態になるときのアニメーションを表示します。 次に例を示します。

public override void WillTransitionToTraitCollection (UITraitCollection traitCollection, IUIViewControllerTransitionCoordinator coordinator)
{
    base.WillTransitionToTraitCollection (traitCollection, coordinator);
    coordinator.AnimateAlongsideTransition ((UIViewControllerTransitionCoordinatorContext) => {
        UpdateConstraintsForTraitCollection (traitCollection);
        View.SetNeedsLayout ();
    }, (UIViewControllerTransitionCoordinatorContext) => {
    });
}

トレイト環境のオーバーライド

上で示したとおり、Adaptive Photos アプリケーションでは、iPhone デバイスが横向きモードのとき、分割ビュー コントローラーに詳細ビューとマスター ビューの両方を強制的に表示します。

これは、ビュー コントローラーに関する次のコードで実現されています。

private UITraitCollection forcedTraitCollection = new UITraitCollection ();
...

public UITraitCollection ForcedTraitCollection {
    get {
        return forcedTraitCollection;
    }

    set {
        if (value != forcedTraitCollection) {
            forcedTraitCollection = value;
            UpdateForcedTraitCollection ();
        }
    }
}
...

public override void ViewWillTransitionToSize (SizeF toSize, IUIViewControllerTransitionCoordinator coordinator)
{
    ForcedTraitCollection = toSize.Width > 320.0f ?
         UITraitCollection.FromHorizontalSizeClass (UIUserInterfaceSizeClass.Regular) :
         new UITraitCollection ();

    base.ViewWillTransitionToSize (toSize, coordinator);
}

public void UpdateForcedTraitCollection ()
{
    SetOverrideTraitCollection (forcedTraitCollection, viewController);
}

分割ビュー コントローラーの展開と折りたたみ

次は、分割ビュー コントローラーの展開と折りたたみの動作を Xamarin で実装する方法について見ていきます。 AppDelegate では、分割ビュー コントローラーの作成時、この変化を処理するためのデリゲートを次のように割り当てています。

public class SplitViewControllerDelegate : UISplitViewControllerDelegate
{
    public override bool CollapseSecondViewController (UISplitViewController splitViewController,
        UIViewController secondaryViewController, UIViewController primaryViewController)
    {
        AAPLPhoto photo = ((CustomViewController)secondaryViewController).Aapl_containedPhoto (null);
        if (photo == null) {
            return true;
        }

        if (primaryViewController.GetType () == typeof(CustomNavigationController)) {
            var viewControllers = new List<UIViewController> ();
            foreach (var controller in ((UINavigationController)primaryViewController).ViewControllers) {
                var type = controller.GetType ();
                MethodInfo method = type.GetMethod ("Aapl_containsPhoto");

                if ((bool)method.Invoke (controller, new object[] { null })) {
                    viewControllers.Add (controller);
                }
            }

            ((UINavigationController)primaryViewController).ViewControllers = viewControllers.ToArray<UIViewController> ();
        }

        return false;
    }

    public override UIViewController SeparateSecondaryViewController (UISplitViewController splitViewController,
        UIViewController primaryViewController)
    {
        if (primaryViewController.GetType () == typeof(CustomNavigationController)) {
            foreach (var controller in ((CustomNavigationController)primaryViewController).ViewControllers) {
                var type = controller.GetType ();
                MethodInfo method = type.GetMethod ("Aapl_containedPhoto");

                if (method.Invoke (controller, new object[] { null }) != null) {
                    return null;
                }
            }
        }

        return new AAPLEmptyViewController ();
    }
}

SeparateSecondaryViewController メソッドで、写真を表示するかどうかをテストし、その状態に応じてアクションを実行します。 写真を表示しない場合は、セカンダリ ビュー コントローラーを折りたたみ、マスター ビュー コントローラーを表示します。

分割ビュー コントローラーが展開されるときについては、CollapseSecondViewController メソッドで、スタック内に写真が存在するかどうかを確認し、存在する場合は折りたたんでそのビューに戻しています。

ビュー コントローラー間の移動

Adaptive Photos アプリケーションでビュー コントローラー間の移動がどのように行われるかを説明します。 AAPLConversationViewController クラスでは、テーブルのセル選択操作がユーザーによって行われると、ShowDetailViewController メソッドが呼び出されて詳細ビューが表示されます。

public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
{
    var photo = PhotoForIndexPath (indexPath);
    var controller = new AAPLPhotoViewController ();
    controller.Photo = photo;

    int photoNumber = indexPath.Row + 1;
    int photoCount = (int)Conversation.Photos.Count;
    controller.Title = string.Format ("{0} of {1}", photoNumber, photoCount);
    ShowDetailViewController (controller, this);
}

開示インジケーターの表示

Adaptive Photo アプリケーションでは、開示インジケーターを数か所に配置し、トレイト環境の変化に応じてそれらの表示と非表示を切り替えています。 その処理を行うコードは次のとおりです。

public bool Aapl_willShowingViewControllerPushWithSender ()
{
    var selector = new Selector ("Aapl_willShowingViewControllerPushWithSender");
    var target = this.GetTargetViewControllerForAction (selector, this);

    if (target != null) {
        var type = target.GetType ();
        MethodInfo method = type.GetMethod ("Aapl_willShowingDetailViewControllerPushWithSender");
        return (bool)method.Invoke (target, new object[] { });
    } else {
        return false;
    }
}

public bool Aapl_willShowingDetailViewControllerPushWithSender ()
{
    var selector = new Selector ("Aapl_willShowingDetailViewControllerPushWithSender");
    var target = this.GetTargetViewControllerForAction (selector, this);

    if (target != null) {
        var type = target.GetType ();
        MethodInfo method = type.GetMethod ("Aapl_willShowingDetailViewControllerPushWithSender");
        return (bool)method.Invoke (target, new object[] { });
    } else {
        return false;
    }
}

実装には、既に詳しく説明した GetTargetViewControllerForAction メソッドを使用します。

テーブル ビュー コントローラーでは、上記のように実装したメソッドで、データが表示されているときにプッシュが発生するかどうか、それに応じて開示インジケーターの表示と非表示をどのように切り替えるかを確認しています。

public override void WillDisplay (UITableView tableView, UITableViewCell cell, NSIndexPath indexPath)
{
    bool pushes = ShouldShowConversationViewForIndexPath (indexPath) ?
         Aapl_willShowingViewControllerPushWithSender () :
         Aapl_willShowingDetailViewControllerPushWithSender ();

    cell.Accessory = pushes ? UITableViewCellAccessory.DisclosureIndicator : UITableViewCellAccessory.None;
    var conversation = ConversationForIndexPath (indexPath);
    cell.TextLabel.Text = conversation.Name;
}

新しい ShowDetailTargetDidChangeNotification タイプ

分割ビュー コントローラー ShowDetailTargetDidChangeNotification 内でサイズ クラスとトレイト環境を扱うために使用できる新しい通知タイプが、Apple によって追加されました。 この通知は、分割ビュー コントローラーのターゲットの詳細ビューに変化 (コントローラーの展開、折りたたみなど) が発生するたびに送出されます。

Adaptive Photos アプリケーションでは、この通知を利用して、詳細ビュー コントローラーが変化したときに開示インジケーターの状態を更新しています。

public override void ViewDidLoad ()
{
    base.ViewDidLoad ();
    TableView.RegisterClassForCellReuse (typeof(UITableViewCell), AAPLListTableViewControllerCellIdentifier);
    NSNotificationCenter.DefaultCenter.AddObserver (this, new Selector ("showDetailTargetDidChange:"),
        UIViewController.ShowDetailTargetDidChangeNotification, null);
    ClearsSelectionOnViewWillAppear = false;
}

Adaptive Photos アプリケーションを詳しく調べると、Xamarin.iOS ではサイズ クラス、トレイト コレクション、適応型ビュー コントローラーを使用して簡単に統合アプリケーションを作成できることがわかります。

統合ストーリーボード

iOS 8 の統合ストーリーボードでは、複数のサイズ クラスをターゲットにして単一の統合ストーリーボード ファイルを作成し、iPhone と iPad の両デバイスに対応することができます。 統合ストーリーボードを使用すると、特定の UI に特化したコードを記述する必要性を減らし、作成と保守の対象になるインターフェイス デザインを 1 つにまとめることができます。

統合ストーリーボードの主な利点は以下のとおりです。

  • iPhone と iPad で同じストーリーボード ファイルを共用できます。
  • iOS 6 と iOS 7 を対象とした後方へのデプロイが可能です。
  • 異なるデバイスの種類、画面の向き、OS バージョンに対応するさまざまなレイアウトをプレビューできます。

サイズ クラスの有効化

Xamarin.iOS プロジェクトを新規に作成するときは、既定でサイズ クラスが使用されます。 古いプロジェクトのストーリーボード内でサイズ クラスと適応型セグエを使用するには、まず Xcode 6 統合ストーリーボード形式に変換し、その後、ストーリーボードの Xcode ファイル インスペクターで [Use Size Classes (サイズ クラスを使用する)] チェック ボックスをオンにする必要があります。

動的起動画面

起動画面ファイルは、iOS アプリケーションの起動時にスプラッシュ画面として表示され、アプリの起動処理が実際に進んでいることをユーザーに知らせます。 iOS 8 より前のバージョンでは、開発者が、アプリケーションを実行するデバイスの種類、画面の向き、解像度ごとに複数の Default.png 画像アセットを用意する必要がありました。 このため、たとえば Default@2x.pngDefault-Landscape@2x~ipad.pngDefault-Portrait@2x~ipad.png のような一連のファイルが用意されていました。

新しい iPhone 6 と iPhone 6 Plus デバイス (および、今後登場する Apple Watch) と、既存の iPhone および iPad デバイスをすべて含めて考えれば、作成および保守が必要となる Default.png 起動画面用画像アセットのサイズ、向き、解像度のバリエーションは膨大な種類になります。 しかも、それらのファイルは非常にサイズが大きい場合があり、配布可能アプリケーション バンドルの "肥大化" につながります。これは、エンド ユーザーから見ると、iTunes App Store からアプリケーションをダウンロードする際の所要時間が長くかかり (セルラー ネットワーク経由での提供が難しくなる可能性があり)、デバイス上のストレージ占有量も大きくなることを意味します。

iOS 8 では、開発者が Xcode で単一のアトミックな .xib ファイルを作成できるようになりました。このファイルから自動レイアウト クラスとサイズ クラスに基づいて 動的起動画面が作成されるため、すべてのデバイス、解像度、向きに対応できます。 これには、必要とされるすべての画像アセットを作成し保守する開発者の作業負荷が軽減されるのに加え、インストールされるアプリケーション バンドルのサイズも大幅に抑えられるメリットがあります。

動的起動画面については、以下に示す制限事項と注意事項があります。

  • UIKit のクラスだけを使用すること。
  • 使用するルート ビューは、UIView または UIViewController オブジェクト 1 つだけにすること。
  • アプリケーションのコードとは一切関連付けないこと (アクションアウトレットを追加しないこと)。
  • UIWebView オブジェクトを追加しないこと。
  • カスタム クラスを使用しないこと。
  • ランタイム属性を使用しないこと。

以上のガイドラインを念頭に置きつつ、既存の Xamarin iOS 8 プロジェクトに動的起動画面を追加する方法を見てみましょう。

次の操作を行います。

  1. Visual Studio for Mac を開き、動的起動画面を追加するソリューションを読み込みます。

  2. ソリューション エクスプローラーで、MainStoryboard.storyboard ファイルを右クリックし、[Open With (プログラムから開く)]>[Xcode Interface Builder] を選択します。

    Xcode Interface Builder で開く

  3. Xcode で、[File (ファイル)]>[New (新規)]>[File... (ファイル...)] を選択します。

    [ファイル]/[新規作成] を選択する

  4. [iOS]>[User Interface (ユーザー インターフェイス)]>[Launch Screen (起動画面)] を選択し、[Next (次へ)] ボタンをクリックします。

    [iOS]/[ユーザー インターフェイス]/[起動画面] を選択する

  5. ファイルに LaunchScreen.xib という名前を付け、[Create (作成)] ボタンをクリックします。

    ファイルに LaunchScreen.xib という名前を付ける

  6. 起動画面のデザインを編集するには、グラフィック要素を追加し、レイアウト制約を使用して、デバイスの種類、画面の向きとサイズに応じた配置を行います。

    起動画面のデザインの編集

  7. LaunchScreen.xib に対する変更を保存します。

  8. アプリケーション ターゲット[General (一般)] タブを選択します。

    [アプリケーション ターゲット] と [General (一般)] タブを選択します

  9. [Choose Info.plist File... (Info.plist ファイルの選択...)] ボタンをクリックし、この Xamarin アプリの Info.plist を選択して、[Choose (選択)] ボタンをクリックします。

    Xamarin アプリの Info.plist を選択する

  10. [App Icons and Launch Images (アプリ アイコンと起動画像)] セクションで、[Launch Screen File (起動画面ファイル)] ドロップダウンを開き、上で作成した LaunchScreen.xib を選択します。

    LaunchScreen.xib を選択する

  11. ファイルへの変更を保存し、Visual Studio for Mac に戻ります。

  12. Visual Studio for Mac と Xcode の間で変更内容の同期が完了するのを待ちます。

  13. ソリューション エクスプローラーで、Resource フォルダーを右クリックし、[Add (追加)]>[Add Files... (ファイルの追加...)] を選択します。

    [追加]/[ファイルの追加]を選択します。..

  14. 上で作成した LaunchScreen.xib ファイルを選択し、[Open (開く)] ボタンをクリックします。

    LaunchScreen.xib ファイルを選択する

  15. アプリケーションをビルドします。

動的起動画面のテスト

Visual Studio for Mac で、iPhone 4 Retina シミュレーターを選択し、アプリケーションを実行します。 動的起動画面が適切な形式と向きで表示されます。

垂直方向に表示された動的起動画面

Visual Studio for Mac 内のアプリケーションを停止し、いずれかの iPad iOS 8 デバイスを選択します。 アプリケーションを実行すると、起動画面がこのデバイスと向きに合った適切な形式で表示されます。

水平方向に表示された動的起動画面

Visual Studio for Mac に戻り、アプリケーションの実行を停止します。

iOS 7 への対応

iOS 7 との下位互換性を維持するには、通常の Default.png 画像アセット群を通常の方法で iOS 8 アプリケーションに含めます。 iOS 7 デバイスで実行すると、iOS は以前の動作に戻り、これらのファイルが起動画面として使用されます。

まとめ

この記事では、各種のサイズ クラスと、それらが iPhone および iPad デバイスのレイアウトに及ぼす影響について概要を述べました。 次に、トレイト、トレイト環境、トレイト コレクションがサイズ クラスと共に機能して統一インターフェイスが実現されるしくみを説明しました。 また、適応型ビュー コントローラーの概要と、それらが統一インターフェイスの中でサイズ クラスと連携して機能する様子を概観しました。 さらに、Xamarin iOS 8 アプリケーション内で C# コードだけを使用してサイズ クラスと統一インターフェイスを実装する方法を説明しました。

この記事の最後では、動的起動画面を 1 つ作成し、すべての iOS 8 デバイスで起動画面として表示させる方法について基本事項を説明しました。