UISplitViewController を実装する : Objective-C プログラミング

PROGRAM


UISplitViewController を実装する

iPad で UISplitViewController を使ってみた時に、実装で少し戸惑うところがあったので、その辺りを中心に UISplitViewController の使い方について整理しておこうと思います。

UISplitView というのは、横にした画面を左と右とに分けて、片方でメニューを、もう片方で各メニュー項目の内容を表示するビューになります。画面を縦にした場合は、メニュー画面がポップオーバーメニューとして隠されて、内容の画面だけが大きく表示されます。

    

UISplitView を配置する

まず、UISplitViewController は、いちばん最初のビューコントローラーとして実装する必要があるようでした。

今回は次のような感じで、UISplitViewController からいったん UINavigationController を挟んで、その上で Master View Controller であれば、メニュー用に UITableViewController を配置します。Detail View Controller にもいったん UINavigationController を挟んで、その上に、コンテンツ表示用の UIViewController を配置します。

Detail View Controller とコンテンツ用の UIViewController との間には、必ずしも UINavigationController を挟まなくても良いのですけど、コンテンツとして表示している画面のタイトルを表示した方がカッコが付く都合、ここでは UINavigationController を挟むようにしてみます。

UISplitViewController を初期化する

インターフェイスビルダーで上記の構造を作ったら、まずはそれをアプリケーションのメインのインターフェイスにします。

プロジェクトの "TARGETS" 設定から、用意したものが Storyboard であれば "Main Storyboard" に、NIB (XIB) であれば "Main Interface" に、UISplitViewController のインターフェイスを指定します。

 

続いて、アプリケーションの UIApplicationDelegate の実装で、UISplitViewController のデリゲートを設定します。

今回は UISplitViewController の制御 (UISplitViewControllerDelegate) を、コンテンツを表示するための delegateViewController (UIViewController) に実装しようと思うので、UIApplicationDelegate の "application:didFinishLaunchingWithOptions:" に、次のような初期化コードを記載します。

 

AppDelegate.m

// アプリ起動時に UISplitViewController 関連の初期化を行います。

- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions

{

// iPad での動作の場合に UISplitViewController の初期化を行います。

if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad)

{

// UISplitViewController はウィンドウ上の最初のビューコントローラーになります。

UISplitViewController* splitViewController = (UISplitViewController*)self.window.rootViewController;

 

// detailViewController は、UISplitView の Detail View Controller に設定された Root View Controller になります。

UIViewController* detailViewController = [[splitViewController.viewControllers lastObject] topViewController];

}

}

これで、UISplitViewController に関する通知を detailViewController で受けることができるようになります。

 

そして detailViewController の側でこの SplitViewControllerDelegate を扱えるように、次のような UIViewController から派生させたクラスを作成します。今回はそれを "EzDelegateViewController" という名前で作成しました。

 

EzDetailViewController.h

// detailViewController 用の UIViewController として使用します。

#import <UIKit/UIKit.h>

 

@interface EzDetailViewController : UIViewController <UISplitViewControllerDelegate>

{

// 表示中のビューを保持するのに使用します。

UIViewController* _shownViewController;

}

 

// 表示中のビューをコントロールするのに使用するプロパティです。

@property (strong, nonatomic) UIViewController* shownViewController;

 

// UISplitViewController が縦画面表示のときの、ポップアップメニューを管理します。

@property (strong, nonatomic) UIPopoverController* masterPopoverController;

 

@end

実装部分としてはとりあえず、ポップアップメニューのプロパティを実装しておきます。

 

EzDetailViewController.m

#import "EzDetailViewController.h"

 

@implementation EzDetailViewController

 

// ポップオーバーメニューを保持するプロパティは、単純にインスタンス変数に結び付けておきます。

@synthesize masterPopoverController = _masterPopoverController;

ひとまずここまで出来上がったら、インターフェイスビルダーで、detailViewController として使用するコントローラーのクラスを "UIViewController" から "EzDetailViewController" に置き換えておきます。

    

ここまでの準備ができたら、続いて UISplitViewController の制御について実装して行きます。

ポップオーバーメニューを制御する

UISplitViewController では、横画面では Master View Controller にメニューが表示され、縦画面ではそのメニューを隠しておいて、必要に応じてポップオーバーメニューで表示するというのが、基本的な動きになります。

この制御は UISplitViewControllerDelegate のメソッドを使って、自分で実装する必要があるようです。

今回は UISplitViewControllerDelegate を EzDetailViewController クラスに持たせるようにしているので、そのクラスの実装部分に、次のように、ポップオーバーメニューの制御コードを追加します。

 

EzDetailViewController.m

// Master View Controller が隠される直前に呼び出されます。

- (void)splitViewController:(UISplitViewController*)splitController willHideViewController:(UIViewController*)viewController withBarButtonItem:(UIButtonItem*)barButtonItem forPopoverController:(UIPopoverController*)popoverController

{

// 渡された UIBarButtonItem に表示するタイトルを設定します。

barButtonItem.title = @"MENU";

 

// Detail View Controller (UINavigationController) の左上に、メニューを呼び出すボタンを表示します。

[self.navigationItem setLeftBarButtonItem:barButtonItem animated:YES];

 

// 受け取ったポップオーバーメニューを内部変数に保持します。

self.masterPopoverController = popoverController;

}

 

// Master View Controller が表示される直前に呼び出されます。

- (void)splitViewController:(UISplitViewController*)splitController willShowViewController:(UIViewController*)viewController invalidatingBarButtonItem:(UIButtonItem*)barButtonItem

{

// Detail View Controller (UINavigationController) の左上からボタンを取り除きます。

[self.navigationItem setLeftBarButtonItem:nil animated:YES];

 

// 表示されていたポップオーバーメニューを解放します。

self.masterPopoverController = ni;

}

これで UISplitViewController での、デバイス回転時のメニュー制御についてが仕上がりました。

UISplitViewController の回転を制御する

UISplitViewController の回転は、Master View Controller と Detail View Controller の両方が回転可能の方向にのみ、回転するようになっています。

ただし Master View Controller や Detail View Controller が UINavigationController の場合で、そこで回転可能かどうかの判定を独自に実装しない場合には、それに設定されている rootViewController が回転できるかによって判断されるようでした。

 

ちなみに View Controller がある方向へ回転可能かどうかを判定するには、UIViewController の "shouldAutorotateToInterfaceOrientation:" メソッドをオーバーライドします。

// 引数で指定された方向に回転可能な場合に YES を返します。

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation

{

// ここでは、全方向への回転を許可しています。

return YES;

}

単純に YES を返すことで、全方向への回転を許可する意味になります。

ここでたとえば、縦方向のみを許可したいのであれば "return UIInterfaceIsPortrait(interfaceOrientation)" といった具合です。

Master View Controller から Detail View Controller へアクセスできるようにする

Master View Controller に置かれたメニュー (mainMenuViewController) から、内容を表示する Detail View Controller 上の詳細画面 (detailViewController) へのアクセスは、順を追って辿ればそれほど難しくはない感じです。

ただ、その都度たどってアクセスするのは面倒なので、mainMenuViewController にインスタンス変数を持たせて、そこから簡単にアクセスできるようにしてみます。

なお、ここでは mainMenuViewController として、UITableViewController から派生させたクラス EzMainMenuViewController を使用します。

 

まずは EzMainMenuViewController に、detailViewController を保持するためのインスタンス変数を用意します。

 

EzMainMenuViewController.h

#import <UIKit/UIKit.h>

#import "EzDetailViewController"

 

@interface EzMainMenuTableViewController : UITableViewController

{

EzDetailViewController* _detailViewController;

}

 

// detailViewController をプロパティを介して簡単に取得できるようにします。

@property (strong, nonatomic) EzDetailViewController* detailViewController;

 

@end

EzMainMenuViewController.m

#import "EzMainMenulViewController.h"

 

@implementation EzMainMenuViewController

 

// detailViewController プロパティの値は、変数 _detailViewController に保持するようにします。

@synthesize detailViewController = _detailViewController;

そしてこの detailViewController プロパティには、Detail View Controller 上の EzDetailViewController を取得して格納します。

 

detailViewController の取得については、まず簡単に整理すると、mainMenuViewController の splitViewController プロパティから順を追って detailViewController を辿って行きます。

splitViewController には、Master View Controller と Detail View Controller の 2 つが viewControllers プロパティに登録されているので、そのうちの 2 番目(最後)を取得することで Detail View Controller を取得することができます。

今回は Detail View Controller に UINavigationController を使用していて、その Root View Controller として detailViewController を設定してあるので、Detail View Controller の topViewController プロパティを参照することで、無事 detailViewController にたどり着きます。

 

これをプログラムで表現するととても単純で、mainMenuViewController を self とした場合、次のようにして取得できます。

取得するタイミングとしては、EzMainMenuViewController が読み込まれた直後に呼び出される "viewDidLoad" メソッドあたりが良さそうです。

 

EzMainMenuViewController.m

// EzMainMenuViewController が読み込まれたタイミングで、detailViewController への参照を取得します。

- (void)viewDidLoad

{

[super viewDidLoad];

 

self.detailViewController = (EzDetailViewController*)[[self.splitViewController.viewControllers lastObject] topViewController];

}

これで、mainMenuViewController のプロパティを通して、detailViewController にアクセスする準備が整いました。

detailViewController に UIViewController を表示する

ここまでできたら、いよいよ detailViewController に、任意の UIViewController を表示できるようにしてみます。

そのために、EzDetailViewController に用意した shownViewController プロパティを実装して、ここに設定された UIViewController の内容が表示されるようにします。

 

EzDetailViewController.m

- (UIViewController*)shownViewController

{

// プロパティの取得は、保持している値をそのまま返します。

return _shownViewController;

}

 

- (void)setShownViewController:(UIViewController*)shownViewController

{

// 引数の UIViewController が表示中のものでなければ、表示処理を行います。

if (_shownViewController != shownViewController)

{

// 表示中の UIViewController を非表示にします。

[_shownViewController.view removeFromSuperview];

[_shownViewController removeFromParentViewController];

 

// 新しく表示する UIViewController をインスタンス変数に保持します。

_shownViewController = shownViewController;

 

// 必要であれば、画面サイズのコピーと、全方向の自動サイズ調整 (63) を設定しておきます。

_shownViewController.view.frame = self.view.frame;

_shownViewController.view.autoresizingMask = 63;

 

// Detail View Controller のタイトルを、新しいビューのタイトルにします。

self.title = _shownViewController.title;

 

// 新しい UIViewController の内容を表示します。

[self addChildViewController:_shownViewController];

[self.view addSubview:_shownViewController.view];

}

// ポップオーバーメニューが存在する場合には、それを非表示状態にします。

[self.masterPopoverController dismissPopoverAnimated:YES];

}

これで、detailViewController の shownViewController プロパティに UIViewController を追加することで、その内容が画面に表示されるようになりました。

 

なお、上記では UIViewController の iOS 5.0 以上から利用可能なメソッド "addChildiewController:" と "removeFromParentViewController" を使用しています。

iOS 5.0 未満のアプリなどで、これらのメソッドを利用できない場合には、これらの各行を削除しても基本的には動作します。

ただし、UISplitViewController の回転状態を把握する の中でお話したように、その場合には UISplitViewController の回転状態が detailViewController で表示している shownViewController まで伝わってこなくなるようなので注意が必要です。

回転情報が伝わらないと、その UIViewController で interfaceOrientation プロパティを参照しても正しい方向が得られなかったり、"willRotateToInterfaceOrientation:duration:" といった回転に関するメソッドが呼び出されないので、それを自分のプログラム側でどうにかカバーするようにします。

mainMenuViewController から detailViewController の表示を切り替える

これまでの実装で、detailViewController の表示状態はその shownViewController プロパティを設定することで調整できますし、mainMenuViewController からは detailViewController プロパティを参照することで簡単にそれへアクセスができるようになっています。

そのため、mainMenuViewController の項目がクリックされたときに、その項目に該当する内容を Detail View Controller に表示させたい場合には、UITableViewController の "tableView:didSelectRowAtIndexPath:" メソッドを次のような感じで実装します。

 

EzMainMenuViewController.m

// UITableView のセルがクリックしたときに呼び出されるメソッドです。

- (void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath

{

// セルの選択状態を解除しておきます。

[self.tableView deselectRowAtIndexPath:indexPath animated:YES];

 

// 今回はたとえば、行番号から使用する NIB 名を決定して、その NIB ファイルからインスタンスを生成します。

UIViewController* viewController;

NSString* nibName;

 

switch (indexPath.row)

{

case 0:

nibName = @"viewController1";

break;

 

case 1:

nibName = @"viewController2";

break;

}

 

viewController = [[UIViewController alloc] initWithNibName:nibName bundle:nil];

 

// 生成した UIViewController を Detail View Controller の表示用として設定します。

self.detailViewController.shownViewController = viewController;

}

これで、Master View Controller に表示されているメニューの項目をクリックしたときに、それに該当する内容が Detail View Controller に表示されるようになりました。

起動時に最初の UIViewController を表示する

これまでの実装では、Master View Controller のメニューをタップするまで、Detail View Controller には何も表示されていない状態になっています。

UISplitViewController が初期化された直後から、メニュー項目の最初の内容を Detail View Controller に表示させておきたい場合には、mainMenuViewController の "viewDidLoad:" メソッドの最後あたりに、次のプログラムを追記します。

 

EzMainMenuViewController.m

- (void)viewDidLoad

{

[super viewDidAppear:animated];

 

 

 

// メニュー項目の最初の位置を示す NSIndexPath を作成します。

NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];

 

// 最初のメニュー項目が押されたときの処理を実行します。

[self tableView:self.tableView didSelectRowAtIndexPath:indexPath];

}

これで UISplitViewController が表示されてすぐの段階から、Detail View Controller には、最初のメニューの内容が表示されるようになりました。

ここで、上記のコードをもし "viewDidAppear:" などに実装してしまうと、mainMenuViewController が表示されたタイミング、つまりデバイスが横向きになってメニューが表示されたときに、最初のメニュー項目が表示されるようになってしまうところに注意します。

[ もどる ]