UITableViewController+CoreDataでセクション内セルを削除を行う場合


#概要

セクション内セルを削除するときに、セクション内セルの数が０になる場合はセクションも同時に削除しないと例外が発生します。
しかし、 UITableViewController+CoreDataの組み合わせでは思いがけないところで躓くところが何箇所かあり、自分用のメモとして纏めておきます。

# ハマったところ

## セルの削除ポイント

多くのサンプルコードでtableViewからセルを削除するケースとして

```ObjC
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
  //...
  if (editingStyle == UITableViewCellEditingStyleDelete) {
    [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
  }
}

```

と、上記のデリゲートの中でData Sourceとセルの削除を一緒に行ってるケースが多いですが、CoreDataを使っている場合はCoreDataの削除のみを行います。

```ObjC
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
  if (editingStyle == UITableViewCellEditingStyleDelete) {
    //CoreDataから削除
    NSInteger numRows = [self tableView:tableView numberOfRowsInSection:indexPath.section];
    self.isSectionDeleted = (numRows == 1);
    
    NSManagedObject* managedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
    [[MapData sharedManager].managedObjectContext deleteObject:managedObject];
    [[MapData sharedManager] save];
  } 
}

```

こちら作成中のアプリのコードのスニペットですが、ここでは```fetchedResultsController```から ```managedObject```を取得して削除しています。
途中でセクションデータの削除が必要かどうか判定しているコードがありますが、理由は後述します。

ではセルの削除はどこで行っているのかというと、次のデリゲート内で行っています。

```ObjC
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
  UITableView* tableView = self.tableView;
  switch (type) {      
    case NSFetchedResultsChangeDelete:
      NSLog(@"indexPath.row=%ld section=%ld", indexPath.row, indexPath.section);
      if (self.isSectionDeleted) {
        NSLog(@"isSectionDeleted");
        [tableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationAutomatic];
      }
      
      [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
      
      break;
      
    case NSFetchedResultsChangeUpdate:
      break;
      
    default:
      break;
  }
}

```

ここでの重要なポイントは、セクション内セルの数が０になる場合は```deleteRowsAtIndexPaths```と同じくセクションデータも```deleteSections```で削除しておくことです。
さもなくば、例外が発生しアプリがダウンします。

## セクション内セルの数が０になるときの判定について

さて、セクション内セルの数が０になるときの判定ですが、判定方法についていくつか悩ましい問題があります。
セクション内のセルの数を取得するには```- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
```を使うのがよさそうですが、CoreDataを利用している場合、このメソッドの中身は次のようになっていると思います。

```ObjC
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
  NSArray* sections =  [self.fetchedResultsController sections];
  id <NSFetchedResultsSectionInfo> sectionInfo = [sections objectAtIndex:section];
  return [sectionInfo numberOfObjects];
}
```

このメソッドにセクション内のセル数が0のセクション番号を引数に渡すと例外が発生します。
例外をキャッチしてセクションを0と判定する手段もあるかもしれませんが、基本的にObjective-Cでは例外処理をしたくないので、その手法は使いません。
今回はCoreData内からデータを削除する前に上記のメソッドでセクション内セル数を取得するようにして、セル数が１のときセクション削除が必要だと判定することにしました。

これが```- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
```メソッド内で行っている

```ObjC
NSInteger numRows = [self tableView:tableView numberOfRowsInSection:indexPath.section];
    self.isSectionDeleted = (numRows == 1);
```

の箇所になります。

# まとめ
UITableViewController+CoreDataでのデータの挿入・削除はData Sourceに配列を使ったケースといくぶん違ったルールが適用されます。
上記のように、CoreDataの変更箇所とセルの変更箇所を分けて挿入・削除等の操作することが重要です。