先日見かけたアプリで、UITableViewのヘッダ部分をタッチすると、そのヘッダに属するセルが伸び縮みするという動作がありました。
どうやってやるのか調べてみて、どうやら下の動画のような動きを実装できたので、共有します。
https://dl.dropboxusercontent.com/u/6579646/2010-10-03_1451.swf
まず、ヘッダのタッチを検出する必要があります。UITableViewの派生クラスを作って、
touchesEnded:withEvent:をオーバーライドします。今回のサンプルでは、 UITableViewのデータソースとデリゲートは同一クラスとしています。
各セクションのヘッダの領域は、UITableView#rectForHeaderInSection:で取得できます。
UITableViewDatasourceプロトコルのnumberOfSectionsInTableViewでセクション数を取得して、ヘッダ領域を降順にヒットテストすることで、どのセクションヘッダがタッチされているか(あるいはされていないか)が得られます。
なぜ降順にするかというと、UITableViewのヘッダはしたら上にスクロールさせた際に、見えている一番上のヘッダが画面上部に張り付く挙動をするため、ユーザが実際に触ったヘッダではなく、実際には隠れてしまっているヘッダがヒットするためです。
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{ UITouch* touch = [[touches allObjects] objectAtIndex:0]; CGPoint pt = [touch locationInView:self]; UITableView* tableView = self; ShrinkHeaderTableViewController* controller = self.delegate; for (int i=[self.dataSource numberOfSectionsInTableView:tableView]-1; i >= 0 ; i--) { CGRect headerArea = [tableView rectForHeaderInSection:i]; if(CGRectContainsPoint( headerArea, pt )){ NSLog(@"%s:%d ", __FUNCTION__, __LINE__ ); [controller shrinkRowInSection: i ]; break; } } }
つぎに、当該セクションに属するヘッダ領域を縮めたり展開する方法です。
まず、ヘッダの状態や、セルの表示内容を用意しておきます。ここでは、各セルの表示データや、セクションのタイトルとともに、headerShrinkStatesというNSArrayに格納しています。今回は簡単のため、各配列の要素はNSMutableDictionaryにして、各種セクションの情報を文字列で引っ張るようにています。
- (void)awakeFromNib { [super awakeFromNib]; self.headerShrinkStates = [NSArray arrayWithObjects: [NSMutableDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:YES], @"shrinked", @"cats", @"title", [NSArray arrayWithObjects:@"american shorthair",@"Siamese",@"Egyptian Mau",@"Japanese",nil],@"contents",nil], (中略) }
そして実際に折りたたみ展開を行うメソッドを実装します。
UITableViewControllerの派生クラスに、shrinkRowInSectionというメソッドを実装します。
-(void) shrinkRowInSection:(NSInteger)section { NSLog(@"%s:%d section=%d", __FUNCTION__, __LINE__,section ); UITableView* tableView = (UITableView*)self.view; NSMutableArray* array = [NSMutableArray array]; // (1) prepare to tableView:heightForRowAtIndexPath:, flip and store shrink state BOOL shrinked = [[[self.headerShrinkStates objectAtIndex:section] objectForKey:@"shrinked"] boolValue]; [[self.headerShrinkStates objectAtIndex:section] setObject:[NSNumber numberWithBool: shrinked ? NO :YES] forKey:@"shrinked"]; // (2) store cells to shrink(or expand) to array. for (int i = 0; i < [self tableView:tableView numberOfRowsInSection:section]; i++) { NSIndexPath* indexPath = [NSIndexPath indexPathForRow:i inSection:section]; [array addObject:indexPath]; } [tableView beginUpdates]; // tell table view to cells to shrink(or expand). [tableView reloadRowsAtIndexPaths:array withRowAnimation:YES]; [tableView endUpdates]; }
このメソッドでは二つの処理を行います。
(1)タッチされたセクションの折りたたみ/展開状態を反転させます。 headerShrinkStatesの当該セクションの"shrinked"値を反転させておきます。
(2)あるセクションに属するセルの状態を変更をフレームワークに通知します。
UITableView#reloadRowsAtIndexPaths:withRowAnimationを使って、これは変更の必要なセルのインデクスをindexPathオブジェクトの形でUITableViewに教えます。
このメソッドは、UITableView#beginUpdates から、UITableView#endUpdatesの間で呼び出す必要があります。
UITableView#endUpdatesが呼ばれると、フレームワークがテーブルビュー全体の状態を再構成するので、UITableViewDataSource,UITableViewDelegateのメソッドが呼ばれます。
セルの折りたたみや展開を行うためには、UITableView#endUpdatesの呼び出し後、フレームワークから呼ばれるtableView:heightForRowAtIndexPath:の呼び出しに対して、適切な値を返す必要があります。ここで、0.0fを返すとセルが折りたたまれ、正の値を返すと、セルが展開されます。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { BOOL shrinked = [[[self.headerShrinkStates objectAtIndex:[indexPath section]] objectForKey:@"shrinked"] boolValue]; NSLog(@"%s:%d %d", __FUNCTION__, __LINE__, shrinked ); return shrinked == YES ? 0.0f : CELL_HEIGHT; }
以上で、セクションヘッダをタップすることで、セルの折りたたみ、展開を制御するUIが実現できました。ちょっとHIG的には微妙かもしれませんが、現実問題として、とても多くの内容が入ったテーブルビューを快適にナビゲーションさせるためには有用なテクニックではないでしょうか。
サンプルのソースはこちら