Blockを利用し使いやすくUITableViewをカテゴリで拡張する
UITableViewについて実装の経験がある方は使いづらいと感じたことがあると思います。UITableViewDelegate、UITableViewDataSourceのデリゲートパターンのサイクルは覚えてしまえばすむ話ですが、実装後のメンテに苦労する人も多いのではないでしょうか?
下位実装においては処理単位で実装するように設計しますが、上位実装においては項目単位で属性やイベントの設計を行います。UITableViewのデリゲートパターンの使いにくい点は、Cellの数設定、CellのText設定、Cellの選択時の処理などの処理がそれぞれにまとまり、1項目に対した処理が様々なメソッドに記述しなければならないためです。UIKitのUITableViewでは、NSIndexPathで同じような分岐をそれぞれのDelegateメソッドで記述するので冗長になりやすく、また項目自体が条件により増減する場合は複雑になりがちです。
今回、以前ブログで紹介したBlocksKitのA2DynamicDelegateを利用して、UITableViewでBlockを利用できるようにカテゴリで拡張してみました。
例えば次のような使い方が可能になります。
行設定時の例
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier] autorelease]; } cell.textLabel.text = [_rows objectAtIndex:indexPath.row]; [tableView setHandler:^(UITableView *tv, NSIndexPath *ip){ //Cell選択時の処理 [tv deselectRowAtIndexPath:ip animated:YES]; } forDidSelectRowAtIndexPath:indexPath]; [tableView setHandler:^(UITableView *tv, NSIndexPath *ip){ //アクセサリーボタン押下時の処理 } forAccessoryButtonTappedForRowWithIndexPath:indexPath]; return cell; }
UITableViewのセクション数、セクション毎の行数、セクションヘッダ、フッターの設定
self.tableView = [[[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 480-20-44) style:UITableViewStyleGrouped delegate:self dataSource:self] autorelease]; [self.view addSubview:_tableView]; // Number of Section in TableView. [_tableView setHandlerForNumberOfSectionsInTableView:^NSInteger(UITableView *tv){ return 2; }]; // Part of Section 0 NSInteger section = 0; [_tableView setHandler:^id(UITableView *tv, NSInteger s){ return @"Copy & Paste"; } forTitleForHeaderInSection:section]; [_tableView setHandler:^id(UITableView *tv, NSInteger s){ return @"Menu is shown by LongPress"; } forTitleForFooterInSection:section]; [_tableView setHandler:^NSInteger(UITableView *tv, NSInteger s){ return 2; } forNumberOfRowsInSection:section]; // Part of Section 1 section++; [_tableView setHandler:^id(UITableView *tv, NSInteger s){ return @"Editing"; } forTitleForHeaderInSection:section]; [_tableView setHandler:^id(UITableView *tv, NSInteger s){ return @"EDIT button press."; } forTitleForFooterInSection:section]; [_tableView setHandler:^NSInteger(UITableView *tv, NSInteger s){ return weakSelf.rows.count; } forNumberOfRowsInSection:section];
iOS5で追加されたポップアップメニューの例
//copy function menu is shown by LongPress. [tv setHandler:^BOOL(UITableView *t, NSIndexPath *i){ return YES; } forShouldShowMenuForRowAtIndexPath:ip]; [tv setHandler:^BOOL(UITableView *t, SEL action, NSIndexPath *i, id sender){ return action == @selector(copy:); } forCanPerformActionForRowAtIndexPath:ip]; __block UITableViewCell *weakCell = cell; [tv setHandler:^(UITableView *t, SEL action, NSIndexPath *i, id sender){ [UIPasteboard generalPasteboard].string = weakCell.detailTextLabel.text; } forPerformActionForRowAtIndexPath:ip];
使い方
ソースはこちらになります。
扱いやすいようにBlocksKitと同じMITライセンスにしますので改変含めご自由にご利用ください。
また、BlocksKitを利用しているのでBlocksKitおよびA2DynamicDelegateをプロジェクトに導入してください。導入方法はiOSコーディングスタイルを変えてしまうBlocksKitの紹介を参照してください。
UITableView+BlocksKitExtendsのインポート
利用するControllerでインポートしてください。
#import "UITableView+BlocksKitExtends.h"
UITableViewの初期化
A2DynamicDelegateを利用するため、初期化はUITableView+BlocksKitExtendsの initWithFrame:style:delegate:dataSource: を使ってください。
- (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)style delegate:(id)delegate dataSource:(id)dataSource;
self.tableView = [[[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 480-20-44) style:UITableViewStyleGrouped delegate:self dataSource:self] autorelease];
注意点
標準のデリゲートメソッドの扱い
通常通りにViewControllerにUITableViewDelegate、UITableViewDataSource のデリゲートメソッドを記述しても動作します。UITableView+BlocksKitExtendsでは、通常のデリゲートメソッドを実行、更にsetHandlerで設定されたBlockを実行するようになっています。従って返却値が存在するメソッドについてはBlockで指定したほうが優先されます。また、デリゲートメソッド、Blockが存在しない場合はそれぞれ無視されます。
tableView:canEditRowAtIndexPath: の拡張デリゲートメソッド
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { BOOL ret = NO; id realDelegate = self.realDelegate; if (realDelegate && [realDelegate respondsToSelector:@selector(tableView:canEditRowAtIndexPath:)]) { //通常のデリゲートメソッド ret = [realDelegate tableView:tableView canEditRowAtIndexPath:indexPath]; } NSString *key = [NSString stringWithFormat:kFormat, kHandlerCanEditRow, indexPath.section, indexPath.row]; BKTableViewReturnBoolBlock block = [self.handlers objectForKey:key]; if (block) { //setHandlerで設定されたBlock ret = ((BKTableViewReturnBoolBlock)block)(tableView, indexPath); } return ret; }
大体の拡張デリゲートメソッドはこのようなロジックになっています。
高さ設定用メソッドについて
行、セクションヘッダ、セクションフッタの高さを設定する下記メソッドは、iOSのUITableViewDelegateのサイクル上、表示関連を扱うメソッド以前にコールされるためBlockを設定する機会がないので除外しました。
- UITableViewDelegate#tableView:heightForRowAtIndexPath:
- UITableViewDelegate#tableView:heightForHeaderInSection:
- UITableViewDelegate#tableView:heightForFooterInSection:
デフォルトの高さを変更する際には通常のDelegateパターンによるメソッドを定義してください。
循環参照メモリリークについて
iOSコーディングスタイルを変えてしまうBlocksKitの紹介ででも書きましたが、ブロック外の変数にアクセスする場合は、循環参照を防ぐため__block指定子、__weak修飾子で再定義して参照するようにしてください。(※__weakはARCの場合のみ)
循環参照はInstrumentsで発見できないメモリリークなので注意が必要です。deallocにサウンドBreakPointの設定をお勧めします。
サンプルソース
サンプルソースをGithubにアップしました。
MBBlocksTableViewControllerがUITableView+BlocksKitExtendsを利用したサンプルになります。
https://github.com/hmori/MyBlocks
MBBlocksTableViewControllerで実装しているTableViewの機能は下記になります。
- ヘッダタイトル設定
- フッタの設定
- 行削除
- 行移動
- アクセサリーボタン
- セル選択
- セル内のポップアップメニュー&コピペ(セル長押し)
また無理やりにBlockを使った感満載ですが、loadViewとtableView:cellForRowAtIndexPath: のみでUITableViewに関するすべての処理を記述しました。
尚、このカテゴリはiOS5でしか動作確認をしていません。何か問題がある場合はフィードバックいただけると幸いです。