jarinosuke blog

about software engineering, mostly about iOS

UISplitView, UIPopoverView Tutorial

iPad Programming

iPadアプリケーションの開発はiPhoneでのそれを応用できることも多数ありますが、変更しなくてはいけないものも多いです。
http://developer.apple.com/library/ios/documentation/userexperience/conceptual/mobilehig/MobileHIG.pdf
詳しくはiOS Human Interface Guidlineに書かれていますが、iPadの画面でiPhoneと同様のUIを提供するのはユーザーに対して優しくありません。
たとえばiPhoneではUITableViewを画面全体に表示しますが、それをiPadでも同じ事は良いことではありません。
画面の大きさを利用した適切なUIを提供するべきです。
iPad上での開発ではiPhone上では使えないクラスがいくつかあります。
SplitViewやPopoverViewなどがそれにあたり、これらはiPadの広い画面に適したUIを提供してくれます。

ということで、それらをiPadで有効に活用するために使い方も含めた簡単な説明を以下から始めます。
ちなみにXcode4 + iOS4.3で行っているので若干勝手が違う部分もあるかもしれませんがご理解を。

This Tutorial Flow

このチュートリアルは大きく分けて以下の三つのパートに分かれます。
PART 1.display SplitView.
PART 2.Using PopoverView when device orientation is portrait.
PART 3.Programming PopoverView.

PART 1.display SplitView.

NO XIB

テンプレートにはSplit View-based Applicationが存在しますが、全体像を把握するためにWindow-based ApplicationテンプレートからXIBを使わずに開発しましょう。
みなさんご存じかとは思いますが、僕はたまに忘れるので以下に簡単に書いておきます。

  • New Project→WIndow-based Applicationを作成
  • info.plistの「Main nib file base name」のカラム削除
  • MainWindow.xibを削除
  • main.mを以下のように変更

第四引数をアプリケーション名AppDelegateにする。

int retVal = UIApplicationMain(argc, argv, nil, @"SaveJapanAppDelegate");
  • AppDelegateのdidFinishLaunchingWithOptions内でWindowを作成する。
CGRect screenBounds = [[UIScreen mainScreen] bounds];
 _window = [[UIWindow alloc] initWithFrame:screenBounds];

これでXIB無しで開発する準備が整いました。

What's UISplitView?

ちょっと先に進む前にUISplitViewについてもう少し理解を深めましょう。
UISplitViewは左右にViewControllerを持つViewControllerのControllerのようなものです。
一般的な使い方としては左側にUITableVIewを配置し、そこで発生したイベントを右のViewに反映させます。
左右のViewControllerのイベント通知を行うための方法はいくつかありますが、テンプレート同様、左のViewControllerに右のViewControllerのポインタを持たせることで実装します。

Initialize SplitView

ではSplitViewを作っていきます。
左のUITableViewControllerと、右のUIViewControllerのファイルを新規作成しましょう。

ファイルを追加したら、それらのviewDidLoadメソッドを実装しましょう。

LeftViewController

まずは左側のビューファイルを実装していきましょう。

////.h file/////

#import "RightViewController.h"

//class variable
NSArray *lyrics_;
RightViewController *rightViewController;

@property(nonatomic, retain) RightViewController *rightViewController;
////.m file/////
//viewDidLoad
[super viewDidLoad];

self.title = @"Harder Better Faster Stronger";
 
lyrics_ = [[NSArray alloc] initWithObjects:@"Work it", @"Make it", @"Do it", @"Makes Us",
@"Harder", @"Better", @"Faster", @"Stronger",
@"More than", @"Ever", @"Hour",
@"Our", @"Never", @"Work Is", @"Over",nil];
 
self.clearsSelectionOnViewWillAppear = NO;
self.contentSizeForViewInPopover = CGSizeMake(300.0, 700);


//numberOfRowsInSection
return [lyrics_ count];

//cellForRowAtIndexPath above return
cell.backgroundColor = [UIColor blackColor];
cell.textLabel.textColor = [UIColor colorWithRed:0.25 green:0.8 blue:0.25 alpha:1.0];
cell.textLabel.text = [lyrics_ objectAtIndex:indexPath.row];
cell.textLabel.textAlignment = UITextAlignmentCenter;

//didSelectRowAtIndexPath
 NSString *lyricSelected = [lyrics_ objectAtIndex:indexPath.row];
 self.rightViewController.messageLabel.text = lyricSelected;
//Developer must off the highlight cell, if needed.
 [tableView deselectRowAtIndexPath:indexPath animated:YES];

LeftViewControllerはUITableViewControllerです。
セルに挿入するタイトルをlyrics_に入れています。
TableViewで少し注意しなければいけないのは、セルがタップされてハイライト状態になるのですが、それを解除するための処理は開発者が行わなければいけない点です。
またRightViewControllerのポインタをクラス変数に持たせているのは上記で書いたとおり、これを介して右側のビューを操作するためです。

RIghtViewController

次は右側の主役となるビューを実装していきます。

////.h file////
//class variable
@interface RightViewController : UIViewController <UISplitViewControllerDelegate>{
    UILabel *messageLabel;
 
}
@property(nonatomic, assign)UILabel *messageLabel;
////.m file////

//synthesize
@synthesize messageLabel;

//viewDidLoad
 messageLabel = [[UILabel alloc] initWithFrame:CGRectMake(self.view.bounds.size.width/2 - 150, self.view.bounds.size.height/2 - 50, 300, 100)];
messageLabel.text = @"Punk";
messageLabel.textAlignment = UITextAlignmentCenter;
messageLabel.textColor =  [UIColor colorWithRed:0.25 green:0.8 blue:0.25 alpha:1.0];
messageLabel.backgroundColor = [UIColor blackColor];
messageLabel.font = [UIFont systemFontOfSize:64.0f];
 
self.view.backgroundColor = [UIColor blackColor];
[self.view addSubview:messageLabel];

特に目立って特殊な所はありません。UILabelをプロパティとして宣言することで、左側のテーブルビューコントローラーからの操作を受け付けています。

AppDelegate

ここで二つのビューコントローラーをつなぎ合わせたスプリットビューをウィンドウにアタッチします。

////.h file////

#import "LeftViewController.h"
#import "RightViewController.h"

//Property Declaration
@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) LeftViewController *leftViewController;
@property (nonatomic, retain) RightViewController *rightViewController;
@property (nonatomic, retain) UISplitViewController *splitViewController;
////.m file////
//didFinishLaunchingWithOptions
CGRect screenBounds = [[UIScreen mainScreen] bounds];
_window = [[UIWindow alloc] initWithFrame:screenBounds];
 
self.rightViewController = [[RightViewController alloc] init];
self.rightViewController.title = @"Daft Punk";
 
self.leftViewController = [[LeftViewController alloc] init];
//set right VC pointer to left VC.
self.leftViewController.rightViewController = self.rightViewController;
 
//set lyric's tableView into NavigationVC
UINavigationController *lyricNav = [[[UINavigationController alloc] initWithRootViewController:leftViewController] autorelease];
lyricNav.navigationBar.barStyle = UIBarStyleBlack;
lyricNav.navigationBar.translucent = YES;
 
//set the LR VC to the SplitView
splitViewController = [[UISplitViewController alloc] init];
self.splitViewController.viewControllers = [NSArray arrayWithObjects:lyricNav, self.rightViewController, nil];
self.splitViewController.delegate = self.rightViewController;
 
[self.window addSubview:splitViewController.view];

左のビューコントローラーには右のビューコントローラーのポインタを代入し、それと、左のテーブルビューコントローラーはナビゲーションコントローラーに入れて、スプリットビューコントローラーを作成します。
そして、スプリットビューコントローラーのデリゲート先は上記の通り右のビューコントローラーに設定します。
これでビルドしてみましょう。
ランドスケープモードにすると左側からテーブルビューが出てきます。そして、そのセルをタップすると、右側のラベルにそのセルのタイトルが反映されますね。
f:id:jarinosuke0808:20110329204515p:image

PART 2.Using PopoverView when device orientation is portrait.

ランドスケープモードでは幅が広いのでSplitViewが活躍しますが、同じ幅を取ったままポートレートモードにすると左側が幅を取りすぎてしまいます。なので、スプリットビューコントローラーは傾きがポートレートモードになるのを検知すると左側のビューをしまい、代わりにポップオーバービューをひょうじするボタンを作成するビルトインAPIが提供されています。

このビルトインAPIの実装はとても簡単です。SplitViewのDelegate先である右ビューコントローラーにPopoverViewを表示・非表示させるDelegateメソッドを記述するだけです。

RightViewController
////.h file////
//Add Delegate Protocol
@interface RightViewController : UIViewController <UISplitViewControllerDelegate>

//class variables
UIPopoverController *_popover;
UIToolbar *_toolbar;

//Property Declaration
@property(nonatomic, assign)UIPopoverController *popover;
@property(nonatomic, assign)UIToolbar *toolbar;
////.m file////
//viewDidLoad
self.toolbar = [[UIToolbar alloc] init];
self.toolbar.barStyle = UIBarStyleBlack;
[self.toolbar sizeToFit];
self.toolbar.translucent = YES;
 
UIBarButtonItem *space = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
 
NSArray *buttons = [NSArray arrayWithObjects:space, nil];
[self.toolbar setItems:buttons animated:YES];
 
[self.view addSubview:self.toolbar];

//SplitViewDelegate
//add following method
- (void)splitViewController:(UISplitViewController *)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)pc{
 
    barButtonItem.title = @"lyrics";
    NSMutableArray *items = [[_toolbar items] mutableCopy];
    [items insertObject:barButtonItem atIndex:0];
    [_toolbar setItems:items animated:YES];
    [items release];
    self.popover = pc;
}

- (void)splitViewController:(UISplitViewController *)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem{
 
    self.popover = nil;
}

新しくRightViewControllerにクラス変数としてUIToolbarとUIPopoverControllerを加えています。
そしてSplitViewのデリゲートメソッドとして二つの関数を追加しています。
willHideViewControllerは左のビューが隠れたときすなわちデバイスがポートレートモードに変化したときに実行されます。なので、ここでバーボタンをツールバーに追加し、ポッポオーバービューをクラス変数に代入しています。
willShowViewControllerは上の関数の逆で、左のビューがランドスケープモードになって現れたときに実行されます。

http://twitter.com/#!/jarinosuke/status/51868274618679296
ここで一つ分からない点があったのですが、Toolbarに何もbarButtonItemを付けない状態だと、SplitViewDelegateメソッドがPopoverViewのボタンを表示してくれませんでした。
なので、ここではボタンの配置の設定に用いる目に見えないFlexibleSpaceを加えています。
どのように対処すれば良いか、分かる方がいましたら教えて頂けると助かります。

追加するコードはこれだけです。ビルドしてみましょう。ポートレートモードにすると左上にボタンが表示され、それをタップするとポップオーバービューが表示され中にスプリットビューコントローラーの左側のビューと同じテーブルビューが表示されていると思います。このポップオーバービューの大きさはどこで決めているかというと、実は左側のテーブルビューコントローラーのviewDidLoadメソッド内の

self.contentSizeForViewInPopover = CGSizeMake(300.0, 700);

で設定が可能です。
f:id:jarinosuke0808:20110329204516p:image

PART3.Programming PopoverView.
チュートリアル最後のパートでは右ビューの文字の色を変えるポップオーバービューを一から実装してみます。
さきほどの容量と変わらないので難しくはないはずです。
まずはUITableViewControllerを継承したColorPickerControllerを自作クラスとして作成しましょう。

ColorPickerController
////.h file////
#import <UIKit/UIKit.h>

@protocol ColorPickerDelegate

- (void)colorSelected:(NSString *)color;
@end

@interface ColorPickerController : UITableViewController {
    NSMutableArray *_colors;
    id <ColorPickerDelegate> _delegate;
 
}

@property(nonatomic, retain)NSMutableArray *colors;
@property(nonatomic, assign)id <ColorPickerDelegate> delegate;
@end
////.h file////
//viewDidLoad
self.clearsSelectionOnViewWillAppear = NO;
    self.contentSizeForViewInPopover = CGSizeMake(150.0, 140.0);
    self.colors = [NSMutableArray array];
    [_colors addObject:@"Red"];
    [_colors addObject:@"Blue"];
    [_colors addObject:@"Yellow"];

//numberOfSectionsInTableView
return 1;

//numberOfRowsInSection
return [_colors count];

//cellForRowAtIndexPath
 NSString *color = [_colors objectAtIndex:indexPath.row];
cell.textLabel.text = color;

TableViewControllerを自作した理由はDelegateプロトコルを用意したからです。
そうすることで、右ビューコントローラーにデリゲートさせて色の反映を有効にします。
では、右ビューコントローラーの実装をみてみましょう。

RIghtViewController
////.h file////
//class variables
ColorPickerController *_colorPicker;
UIPopoverController *_colorPickerPopover;
UIBarButtonItem *colorButton;

//Property and Method Declaration
@property(nonatomic, retain) ColorPickerController *colorPicker;
@property(nonatomic, retain)UIPopoverController *colorPickerPopover;

- (void)setColorButtonTapped;
////.m file////
//synthesize
@synthesize colorPicker = _colorPicker;
@synthesize colorPickerPopover = _colorPickerPopover;

//viewDidLoad
colorButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonItemStylePlain target:self action:@selector(setColorButtonTapped)];
 
    NSArray *buttons = [NSArray arrayWithObjects:space, colorButton, nil];

//####ColorPickerDelegate####//
- (void)colorSelected:(NSString *)color{
    if ([color compare:@"Red"] == NSOrderedSame) {
        messageLabel.textColor = [UIColor redColor];
    }else if ([color compare:@"Blue"] == NSOrderedSame ) {
        messageLabel.textColor = [UIColor blueColor];
    }else if ([color compare:@"Yellow"] == NSOrderedSame ) {
        messageLabel.textColor = [UIColor yellowColor];
    }
    [self.colorPickerPopover dismissPopoverAnimated:YES];
}

- (void)setColorButtonTapped{
    if (_colorPicker == nil) {
        self.colorPicker = [[[ColorPickerController alloc] initWithStyle:UITableViewStylePlain] autorelease];
        _colorPicker.delegate = self;
        self.colorPickerPopover = [[[UIPopoverController alloc] initWithContentViewController:_colorPicker] autorelease];
    }
    [self.colorPickerPopover presentPopoverFromBarButtonItem: colorButton permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}

新たにカラーピッカーとポップオーバービューをクラス変数に加えています。
バーボタンももう一つ加え、セレクタにカラーピッカーを生成する関数を指定します。
そしてそのsetColorButtonTapped関数の中でカラーピッカーとポップオーバービューを生成します。
実際に右ビューのラベルの色を変える行為はデリゲートメソッドであるcolorSelectedメソッドで実装しています。
f:id:jarinosuke0808:20110329204517p:image

Show Me the Code!

githubに上記のコードを挙げましたので、もし良かったらみて下さい。
おかしなところなどあったら、指摘して頂けると助かります。
https://github.com/jarinosuke/iPad-Tutorial

参考
http://www.raywenderlich.com/1056/ipad-for-iphone-developers-101-uipopovercontroller-tutorial
http://www.raywenderlich.com/1040/ipad-for-iphone-developers-101-uisplitview-tutorial