目的
自分のデータとテーブル項目の連動(削除、追加)させる方法を学習する。
主要クラス
UITableViewController、NSMutableArray
UITableViewControllerを継承したクラスを使い、自分が定義した独自クラスのインスタンス(以後MyClassと呼ぶ)をNSMutableArrayで管理しテーブル画面に表示する。
また、項目の追加、削除に連動してMyClassインスタンスの追加、削除を連動させる。
本ドリルではMyClassインスタンス配列の管理にNSMutableArrayを使っているが、好み、適正によって別のクラスを使ってもかまわない。
プロジェクトの名称
Table_Navi
サンプル実装説明
テーブルとナビゲーションを理解する(1)で作成したプロジェクトをそのまま流用する。
今回は項目10個のテーブルという固定ではなく、NSMutableArrayが管理しているMyClassインスタンスの数に連動するようにする。
まずはNSMutableArrayをRootViewController.hにinstanceArrayという名称で定義。
@interface RootViewController : UITableViewController {
NSMutableArray* instanceArray;
}
-(void)setInstanceArray:(NSMutableArray*)inArray;
NSMutableArray* instanceArray;
}
-(void)setInstanceArray:(NSMutableArray*)inArray;
また、このinstanceArrayを設定するsetInstanceArray:メソッドは以下のような実装となる。
-(void)setInstanceArray:(NSMutableArray*)inArray {
if (instanceArray != inArray) {
[instanceArray release];
instanceArray = [inArray retain];
}
}
if (instanceArray != inArray) {
[instanceArray release];
instanceArray = [inArray retain];
}
}
retainしているので、deallocでreleaseすることも忘れてはいけない。
- (void)dealloc {
[instanceArray release];
[super dealloc];
}
[instanceArray release];
[super dealloc];
}
独自クラスはNSObjectを継承し内部にNSStringを持たせる。
独自クラスファイルの作成は、画面へのタッチを検出する(1)でのカスタムUIViewの作り方を応用しSub classをUIViewではなくNSObjectにすればよい。
MyClass.h
@interface MyClass : NSObject {
NSString* name;
}
-(void)set:(int)inIndex;
-(NSString*)name;
@end
NSString* name;
}
-(void)set:(int)inIndex;
-(NSString*)name;
@end
MyClass.m
@implementation MyClass
-(void)set:(int)inIndex {
[name release];
name = [NSString stringWithFormat:@"name - %d", inIndex];
[name retain];
}
-(NSString*)name {
return name;
}
-(void)dealloc {
[name release];
[super dealloc];
}
@end
-(void)set:(int)inIndex {
[name release];
name = [NSString stringWithFormat:@"name - %d", inIndex];
[name retain];
}
-(NSString*)name {
return name;
}
-(void)dealloc {
[name release];
[super dealloc];
}
@end
前準備としてTable_NaviAppDelegateのapplicationDidFinishLaunchingでRootViewControllerのinstanceArrayを設定する。
- (void)applicationDidFinishLaunching:(UIApplication *)application {
NSMutableArray* array = [[NSMutableArray alloc] init];
for (int i = 0; i < 20; i++) {
MyClass* m = [[MyClass alloc] init];
[m set:i];
[array addObject:m];
[m release];
}
RootViewController* ctl = (RootViewController*)navigationController.topViewController;
[ctl setInstanceArray:array];
[array release];
・
・
NSMutableArray* array = [[NSMutableArray alloc] init];
for (int i = 0; i < 20; i++) {
MyClass* m = [[MyClass alloc] init];
[m set:i];
[array addObject:m];
[m release];
}
RootViewController* ctl = (RootViewController*)navigationController.topViewController;
[ctl setInstanceArray:array];
[array release];
・
・
ここまでが前準備。
次に本題のMyClassインスタンスを配列で管理するNSMutableArrayとUITableViewとの連携を実装にとりかかる。
これにはRootViewControllerクラスのtableView:numberOfRowsInSection:メソッドとtableView: cellForRowAtIndexPathメソッドを以下のように変更する。
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
return [instanceArray count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
・
・
MyClass* m = [instanceArray objectAtIndex:indexPath.row];
cell.textLabel.text = [m name];
・
・
}
numberOfRowsInSection:(NSInteger)section {
return [instanceArray count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
・
・
MyClass* m = [instanceArray objectAtIndex:indexPath.row];
cell.textLabel.text = [m name];
・
・
}
これで各項目にMyClassの文字列が表示される。
前準備でおこなっていたMyClass配列作成数を20から5に変更してみるなどして、連動する事を確認するとよい。
インスタンス変数の値を取り出すメソッド/設定するメソッドはそれぞれ以下の例のように変数名にそって命名するのが作法となっている。
この作法を守る事でドット構文が利用できる。
ドット構文は「Objective-C 2.0プログラミング言語 >メッセージングの仕組み>プロパティ>ドット構文」を参照。
そのため、上記実装
は
と書いても同じ動作になる。
また、前準備でおこなった
も当然
と記述して問題ない。
例)
CustomClass* abc; 変数名
-(CustomClass*)abc; 変数取り出し用メソッド
-(void)setAbc(CustomClass*)setAbc; 変数設定用メソッド
CustomClass* abc; 変数名
-(CustomClass*)abc; 変数取り出し用メソッド
-(void)setAbc(CustomClass*)setAbc; 変数設定用メソッド
この作法を守る事でドット構文が利用できる。
ドット構文は「Objective-C 2.0プログラミング言語 >メッセージングの仕組み>プロパティ>ドット構文」を参照。
そのため、上記実装
cell.textLabel.text = [m name];
は
cell.textLabel.text = m.name;
と書いても同じ動作になる。
また、前準備でおこなった
[ctl setInstanceArray:array];
も当然
ctl.instanceArray = array;
と記述して問題ない。
削除に対応するには
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath
を実装すればよい。これはNavigation-based Applicationのテンプレートファイルでは注釈にされて最初から提供されている。
このドリルでは単純に[instanceArray removeObjectAtIndex:indexPath.row];を追加して項目の削除に合わせ対応するinstanceArrayのMyClassを削除している。
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[instanceArray removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
} else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class,
// insert it into the array, and add a new row to the table view.
}
}
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[instanceArray removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
} else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class,
// insert it into the array, and add a new row to the table view.
}
}
deleteRowsAtIndexPaths:メソッドより先に
を実行しておく必要がある。deleteRowsAtIndexPaths:メソッドにより画面上の項目が1つ減るのに、numberOfRowsInSectionで返す値が元のままになるのが原因か?
[instanceArray removeObjectAtIndex:indexPath.row];
を実行しておく必要がある。deleteRowsAtIndexPaths:メソッドにより画面上の項目が1つ減るのに、numberOfRowsInSectionで返す値が元のままになるのが原因か?
tableView:commitEditingStyle:forRowAtIndexPath:メソッドもUITableViewDataSourceプロトコルに定義されたメソッドであり「Table View Programming Guide for iPhone OS」に詳しく説明されている。
日本語訳:iPhone OS Table Viewプログラミングガイド 少し版が古い。
上記を実装すると、フリックにより削除ボタンが表示され、ボタンを押すと実際に項目が削除される。
編集ボタンを表示するには- (void)viewDidLoadメソッドで以下の処理を追加する
self.navigationItem.rightBarButtonItem = self.editButtonItem;
この作業だけで、編集ボタンの表示、押された時の編集モードへの移行、完了ボタンへの切り替え、完了ボタンが押された時の編集モード解除および編集ボタンの再表示を受け持ってくれる。
編集ボタンの表示
編集モード
また、フリック時の削除ボタン表示の時に完了ボタンへの移行等もおこなってくれる。
表示が英語表示になる理由はアプリケーションの言語設定を変更していないから。
日本語にしたければTable_Navi-Info.plistのLocalization native development regionをJapanに変更してやればよい。
ただし、英語の環境では英語、日本語の環境では日本語を表示させたい場合、もう少し工夫が必要になる。
項目を追加するためには、ユーザーが追加意思を示すためのGUIが必要。
通常、ナビゲーションバーやツールバーに追加用ボタンを配置しタップに対応する。
ナビゲーションバーへの追加ボタン実装方法は「iPhone OS Table Viewプログラミングガイドの編集モードでの行の挿入と削除の章」に詳しく説明されているので、ここではUIViewTableに付いている追加ボタン表示を使う例を示す。
まず、現在はすべて削除用ボタンになっているところを、2番目の項目だけ追加ボタンに変更する。
これにはtableView:editingStyleForRowAtIndexPath:メソッドを実装すればよい。ここでは項目1だけUITableViewCellEditingStyleInsertを返すようにしている。
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView
editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 1) {
return UITableViewCellEditingStyleInsert;
}
return UITableViewCellEditingStyleDelete;
}
editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 1) {
return UITableViewCellEditingStyleInsert;
}
return UITableViewCellEditingStyleDelete;
}
このメソッドが実装されていない場合、すべての項目はUITableViewCellEditingStyleDeleteとみなされる。
また編集可能/不可能も同様に項目ごとに設定可能。こちらはtableView: canEditRowAtIndexPath:メソッドを実装すればよい。
- (BOOL)tableView:(UITableView *)tableView
canEditRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 0)
return NO;
return YES;
}
canEditRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 0)
return NO;
return YES;
}
ここでは最上位項目だけ編集不可とした。
上記作業で、追加ボタンを押されると、削除ボタンタップ時に呼び出されたtableView:commitEditingStyle:forRowAtIndexPathメソッドが、editingStyle == UITableViewCellEditingStyleInsertとなって呼び出される。
ここで、新しいMyClassインスタンスを作成しinstanceArrayに追加、同じようにUITableView側の項目も追加してやることで実装が完了する。
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
・
・
else if (editingStyle == UITableViewCellEditingStyleInsert) {
// 新しいMyClassインスタンスの作成
MyClass* m = [[MyClass alloc] init];
static int addIndex = 100; // 新規挿入項目だとわかるように100番台にした。
[m set:addIndex++];
[instanceArray insertObject:m atIndex:indexPath.row + 1];
[m release];
[tableView insertRowsAtIndexPaths:
[NSArray arrayWithObject:[NSIndexPath
indexPathForRow:indexPath.row + 1 inSection:indexPath.section]]
withRowAnimation:UITableViewRowAnimationFade];
}
}
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
・
・
else if (editingStyle == UITableViewCellEditingStyleInsert) {
// 新しいMyClassインスタンスの作成
MyClass* m = [[MyClass alloc] init];
static int addIndex = 100; // 新規挿入項目だとわかるように100番台にした。
[m set:addIndex++];
[instanceArray insertObject:m atIndex:indexPath.row + 1];
[m release];
[tableView insertRowsAtIndexPaths:
[NSArray arrayWithObject:[NSIndexPath
indexPathForRow:indexPath.row + 1 inSection:indexPath.section]]
withRowAnimation:UITableViewRowAnimationFade];
}
}
実際には新しいインスタンスのための設定をおこなったりするためのモーダルビュー表示などを組み合わせることになる。モーダルビューについてはモーダルビューを表示する(2)を参照。
挿入する場所を第1項目の後にする事は重要。
前にした場合以下のように最初の項目も追加ボタンが残ったままとなる。
この対応にはinsertRowsAtIndexPathsの後に
で、ずれて一つ下に移動した元第1項目を強制的に更新する必要がある。
ただしアニメーションを無しに指定しているにも関わらずアニメーション表示される。
前にした場合以下のように最初の項目も追加ボタンが残ったままとなる。
この対応にはinsertRowsAtIndexPathsの後に
[tableView reloadRowsAtIndexPaths:
[NSArray arrayWithObject:
[NSIndexPath indexPathForRow:indexPath.row + 1
inSection:indexPath.section]]
withRowAnimation:UITableViewRowAnimationNone];
[NSArray arrayWithObject:
[NSIndexPath indexPathForRow:indexPath.row + 1
inSection:indexPath.section]]
withRowAnimation:UITableViewRowAnimationNone];
で、ずれて一つ下に移動した元第1項目を強制的に更新する必要がある。
ただしアニメーションを無しに指定しているにも関わらずアニメーション表示される。
プロジェクト
検討
この実装では2番目の項目は絶対に削除できない。
どの項目も削除可能にしたければ、「iPhone OS Table Viewプログラミングガイドの編集モードでの行の挿入と削除の章」のように追加ボタンは別の場所に用意するのが適切。