Hatena::Groupiphone-dev

iPhoneアプリ開発まっしぐら★ このページをアンテナに追加 RSSフィード

引っ越し後の日記はコチラです

tokoromのその他の日記

2010-02-14

UITableViewで移動不可のセルを設定する方法の訂正

| 13:03 | はてなブックマーク -  UITableViewで移動不可のセルを設定する方法の訂正 - iPhoneアプリ開発まっしぐら★

UIKit本追加/補足 > UIKit本の内容自体への補足/訂正 > UITableViewで移動不可のセルを設定する方法の訂正


※Twitterで @pactii さんにご指摘いただいての訂正です。@pactiiさん、ありがとうございました!

UIKit本の 9.3.6 セルの移動 (P.301) で一番下のセルを移動不可にする方法として以下のコードを紹介しています。

- (BOOL)tableView:(UITableView*)tableView canMoveRowAtIndexPath:(NSIndexPath*)indexPath {
  // 最後のセル以外ならYES
  return ( dataSource_.count > indexPath.row + 1 );
}

これは、UITableViewDataSourceプロトコルのtableView:canMoveRowAtIndexPath:メソッドで、任意のrowに対する戻り値をNOとすることでそのrowの置き換えを制限するものです。これを利用することで、特定のセルが置き換えできないように指定することができます。9.3.6のサンプルコードでは、一番下の「新規追加」というセルを移動不可に設定しています(図.A)。


◆図.A

f:id:tokorom:20100214130121p:image


しかし、これだけだと、「新規追加」のセルを移動することはできなくても、他のセルを「新規追加」のセルの下に移動することができてしまうことが発覚しました。今回、この点を訂正しお詫びさせていただきます。

他のセルを「新規追加」の下に移動できなくするには、UITableViewDelegateのtableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath:メソッドを使います。

- (NSIndexPath*)tableView:(UITableView*)tableView
    targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath*)sourceIndexPath
    toProposedIndexPath:(NSIndexPath*)proposedDestinationIndexPath
{
  if ( dataSource_.count > proposedDestinationIndexPath.row + 1 ) {
    return proposedDestinationIndexPath;
  } else {
    return sourceIndexPath;
  }
}

これは、セルの移動の挙動を細かく設定するためのメソッドで、

  • 移動前の場所がsourceIndexPath
  • 移動予定の場所がproposedDestinationIndexPath

にそれぞれ渡されてきます。

このメソッドの戻り値に指定した場所が実際の移動先になるため、特に挙動を変更しない場合はproposedDestinationIndexPathをそのまま返します。逆に言えば、移動を許可したくない場合にsourceIndexPathを返してやれば、移動がされなかったという扱いにすることが可能です。

今回で言えば、移動予定の場所(proposedDestinationIndexPath)が「新規追加」の場所よりも下だった場合にsourceIndexPathを返すようにすることで、「新規追加」の場所を常に一番下に保つことが可能になります。



全ての読者の皆様、至らない点があり申し訳ありませんでした。


UITableViewCellにコントロールを追加する方法の訂正

| 13:40 | はてなブックマーク -  UITableViewCellにコントロールを追加する方法の訂正 - iPhoneアプリ開発まっしぐら★

UIKit本追加/補足 > UIKit本の内容自体への補足/訂正 > UITableViewCellにコントロールを追加する方法の訂正


※Twitterで @ytka さんにご指摘いただいての訂正です。@ytkaさん、ありがとうございました!

UIKit本の 9.4.8 セルにコントロールを追加 (P.319) でセルのカスタマイズについて以下のようなコードを紹介しています。


■誤ったコード

- (UITableViewCell*)tableView:(UITableView*)tableView
  cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
  static NSString* identifier = @"basis-cell";
  UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:identifier];
  if ( nil == cell ) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                  reuseIdentifier:identifier];
    [cell autorelease];
  }
  cell.textLabel.text =
    [[dataSource_ objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
  switch ( indexPath.section ) {
    case 0: //< セルにUIImageViewを追加
      [cell.contentView addSubview:[self imageViewForCell:cell withFileName:@"Samurai.png"]];
      break;
    case 1: //< セルにUISwitchを追加
      [cell.contentView addSubview:[self switchForCell:cell]];
      break;
    case 2: //< セルにUISliderを追加
      [cell.contentView addSubview:[self sliderForCell:cell]];
      break;
    default:
      break;
  }
  return cell;
}

これは、図.Bのようにセルに様々なコントロールを追加するためのサンプルコードです。

図.B

f:id:tokorom:20100214133638p:image


これは誤ったコードで、この例のようにセルの再利用が発生しにくいケースでこそ問題は顕在化しませんが、セルの再利用が発生したときに、その再利用されたセルに次々とコントロールがaddSubviewされて画面がぐちゃぐちゃになってしまいます。

このように、dequeueReusableCellWithIdentifier:メソッドで取得したセルに対してコントロールを追加することは基本的にやってはいけません。dequeueReusableCellWithIdentifier:メソッドで取得したセルに対しては、値の変更のみ行うのが正しいやりかたです。

修正したサンプルコードを以下に添付させていただきます。

ここではセルを作成する流れがわかる箇所のみ抜粋しています。全てのコードは、

に置いてありますので、全体が必要な場合にはお手数ですがこちらをご参照ください。

UIKit本をお持ちのかたは、Subversionから更新していただくことで、この修正を含めた全サンプルコードをダウンロードいただけます。

□修正後のサンプルコード

- (UITableViewCell*)tableView:(UITableView*)tableView
  cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
  // カスタムセルの種類によって再利用のためのIDを変えること
  static const id identifiers[3] = { @"image-cell", @"switch-cell", @"slider-cell" };
  NSString* identifier = identifiers[ indexPath.section ];
  UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:identifier];
  if ( nil == cell ) {
    // セルにコントロールを追加する場合は、セルの作成時に行う
    // ここでは各カスタムセルごとにUITableViewCellのサブクラスを定義して使っている
    // 各サブクラスの実装は http://iphone-book-sample.googlecode.com/svn/trunk/Chapter9/TableSample/Classes/SampleForCustomizedCell.m 参照
    // なお、各initXXXメソッド内でそれぞれのセルに必要なコントロールをaddSubviewしている
    switch ( indexPath.section ) {
      case 0:
        cell = [[[CellWithImageView alloc] initWithReuseIdentifier:identifier] autorelease];
        break;
      case 1:
        cell = [[[CellWithSwitch alloc] initWithReuseIdentifier:identifier] autorelease];
        break;
      case 2:
      default:
        cell = [[[CellWithSlider alloc] initWithReuseIdentifier:identifier] autorelease];
        [(CellWithSlider*)cell setDelegate:self];
        break;
    }
  }
  // 再利用のときにも通るコードでは、各コントロールの値の変更のみ行う
  switch ( indexPath.section ) {
    case 0:
      [[cell imageView] setImage:[UIImage imageNamed:@"Samurai.png"]];
      break;
    case 2:
      {
        // UISlider付のセルなら値を設定
        NSNumber* value = [self.sliderValues objectAtIndex:indexPath.row];
        CellWithSlider* cellWithSlider = (CellWithSlider*)cell;
        cellWithSlider.slider.value = [value floatValue];
        cellWithSlider.row = indexPath.row;
      }
      break;
    default:
      break;
  }
  cell.textLabel.text =
    [[self.dataSource objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
  return cell;
}

全ての読者の皆様、至らない点があり申し訳ありませんでした。

notoroidnotoroid2010/03/29 18:12楽しく拝見させていただきました。ドロップ中に「新規追加」の場所を維持する場合、sourceIndexPath ではなく[NSIndexPath indexPathForRow:proposedDestinationIndexPath.row-1 inSection:proposedDestinationIndexPath.section] を返すのはいかがでしょう?

tokoromtokorom2010/03/29 22:21なるほど、そうすると(一番下の)新規追加のところに移動できなくても、そのすぐ上に移動することになるわけですね^^
名案ありがとうございます!

muriyarikayomuriyarikayo2011/08/03 19:36こんにちは。
画面に表示しきれない数のCellがある場合は、notoroidさんのおっしゃるように、
現在表示されているCellのindexPathを返さないと、非表示のCellのキャッシュが出来てしまいUIが崩壊します。

grxqcblnwpgrxqcblnwp2014/02/04 23:06fyixpjqipof.efw, <a href="http://www.djlxctovam.com/">slmmwekfjh</a> , [url=http://www.cjdqexhwco.com/]yvkvuhadyt[/url], http://www.rmwwdekfmq.com/ slmmwekfjh