iOS (iPhone/iPad) アプリ開発で UITableView と UINavigationView を組合せて選択式メニューのような機能を作る

ほぼ個人的なメモ。

  • UITableViewController 側から、自分を管理している UINavigationViewController のインスタンスを参照するときは、self.navigationController を使う。
  • 下の階層 (newController) に進ませるときは、[self.navigationController pushViewController: newController animated: YES]; とする。
  • 強制的に戻るときは、[self.navigationController popViewControllerAnimated: YES ]; とする。
  • TableView の項目は、強制的に描画させないかぎり、最初に表示されたときの状態から変わらない。
    • 選択したメニューにあわせて表示項目を変えたい場合は、[self.tableView reloadData]: として強制的に再描画させる必要がある。
  • 選択式メニュー(メニューの項目の一覧を与えて、その項目のどれが選択されたかという値を返すような、汎用のクラスは提供されていない(たぶん)。

以下、ARC なしで iOS の Empty アプリケーションを作って、UITableViewController のサブクラスとして FirstViewController クラスと SelectTableController という二つのクラスを作る例を挙げてみる。SelectTableController を、汎用的に使える選択式メニューにする。IB は使っていない。

この例では UITableViewController のインスタンスをArrayにすべて保持する方法をとっている。選択肢が多くなると、この方法ではメモリを圧迫する可能性がある。これを避けるには、他のところで書いてるように Core Data を使ってデータを管理するか、viewWillAppear, viewWillDisappear をうまくつかって処理する方法がある。

ソース例

  • AppDelegate.h

UINavigationViewController と、それにセットする UITableViewController のサブクラスを作成する。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];

    // トップレベルの TableViewController    
    FirstViewController *first = [[FirstViewController alloc] initWithStyle: UITableViewStylePlain ];

    UINavigationController *navi1 = [[UINavigationController alloc] initWithRootViewController:first ];

    // Windows の rootViewController として navi1 をセット
    [self.window setRootViewController: navi1];
    
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}
  • FirstViewController.h
@interface FirstViewController : UITableViewController
{
    // メニューの項目(SelectViewController)のリストを保持する
    NSMutableArray *childTables;
}
@end
  • FirstViewController.m
#import "FirstViewController.h"
#import "SelectTableController.h"

@implementation FirstViewController

// 子メニューに使う選択肢の文字列リスト
static NSString* letters[4]  = {@"A",@"B",@"C",@"D"};

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        childTables = [[NSMutableArray alloc] init];

        for ( int i = 0; i < 10; i++ )
        {
            // 子メニューに与える選択肢の一覧を Array にセットする。
	    // この配列の内容を変更することで、子メニューでの選択肢の内容を変化させられる。
            NSArray *strs = [NSArray arrayWithObjects: 
                            [NSString stringWithFormat: @"%d: %@", i, letters[0] ],
                            [NSString stringWithFormat: @"%d: %@", i, letters[1] ],
                            [NSString stringWithFormat: @"%d: %@", i, letters[2] ],
                            [NSString stringWithFormat: @"%d: %@", i, letters[3] ], nil ];
            
	    // 子メニューを作って childTables に保持しておく。
            SelectTableController *child = [[SelectTableController alloc] 
                                            initWithStyle:UITableViewStylePlain stringArray: strs];
            [childTables addObject: child];
        }
    }
    return self;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

#pragma mark - View lifecycle

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self.navigationItem setTitle: @"First"];
}

- (void)viewDidUnload
{
    [super viewDidUnload];
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
}

// 画面遷移などで self.tableView が表示される時に呼ばれる
- (void)viewDidAppear:(BOOL)animated
{
    // 強制的にメニューの項目をすべて再表示させる。
    // これをしないと、子メニューで選択された項目の内容が表示に反映されない。
    [self.tableView reloadData];

    [super viewDidAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1; // この例ではセクション数は 1 つ
}

// 保持するメニューの項目数を返す。
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [childTables count];
}

// メニュー項目を表示する
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }
    
    // 右側に矢印のアイコンをつける
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

    SelectTableController *sel = [childTables objectAtIndex:[indexPath row]];

    // 子メニューでいずれかの項目を選択済なら、その項目の内容に対応する文字を表示させる。
    // 何も選択されていなければ、未選択と表示する。
    if ( sel.selectedNumber >= 0 ){
        [cell.textLabel setText: [NSString stringWithFormat: @"%d: 選択: %@", 
                                  [indexPath row],
                                  letters[sel.selectedNumber] ]];
    }else{
        [cell.textLabel setText: [NSString stringWithFormat: @"%d: 未選択",
                                  [indexPath row] ]];
    }
    
    return cell;
}


#pragma mark - Table view delegate

// 項目が選択されたときに呼びだされる
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 選択された項目に対応する SelectTableController を遷移する画面として
    // self. navigationController に push する。
    [self.navigationController pushViewController:[childTables objectAtIndex:[indexPath row]]
                                         animated: YES ];
}
@end
  • SelectTableController.h
@interface SelectTableController : UITableViewController
{
    // 選択肢のメニュー(文字列)nの一覧を保持する
    NSArray* array;

    // 選択されたメニューの番号を保持する。
    NSInteger selectedNumber;
}

// 外部のインスタンスから選択されたメニューの番号を参照できるようにする。
@property (nonatomic, assign) NSInteger selectedNumber;

// 選択肢のメニュー(文字列/NSString)を stringArray として与えて初期化できるメソッドを用意
- (id)initWithStyle:(UITableViewStyle)style stringArray:(NSArray*)stringArray;

@end
  • SelectTableController.m
#import "SelectTableController.h"

@implementation SelectTableController

// selectedNumber のアクセサ(getter/setter)を生成
@synthesize selectedNumber;

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

// メニューの項目一覧を stringArray として受け取って初期化
- (id)initWithStyle:(UITableViewStyle)style stringArray:(NSArray*)stringArray;
{
    self = [super initWithStyle:style];
    if ( self ){
        // retain して array を保持する。
        array = [stringArray retain];
        selectedNumber = -1; // -1 は未選択状態を意味することにする。
    }
    return self;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

#pragma mark - View lifecycle

- (void)viewDidLoad
{
    [super viewDidLoad];
}

- (void)viewDidUnload
{
    [super viewDidUnload];
    [array release]; // ratin に対応して release する。
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [array count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }

    // メニューの項目(文字列)の設定    
    [cell.textLabel setText: [array objectAtIndex:[indexPath row]]];
    
    return cell;
}


#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // メニューが選択さたときに、その番号を保持しておく。
    selectedNumber = [indexPath row];

    // 強制的にひとつ上の階層に戻す。これをコメントアウトすると、選択しただけでは戻らなくなる。
    [self.navigationController popViewControllerAnimated: YES ];
}

@end