ぽんぽこ日記

プログラミング、読書、日々の生活

UITableViewのヘッダーへのタッチでセルを折りたたむ方法

先日見かけたアプリで、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的には微妙かもしれませんが、現実問題として、とても多くの内容が入ったテーブルビューを快適にナビゲーションさせるためには有用なテクニックではないでしょうか。

サンプルのソースはこちら