Study CoreData 5 ~まだまだ序盤~

みなさん、おはこんちわんばんこ!

iPhoneSDKに取り付かれたオトコ、Jacminikです。

あらためて確認しておきますが、このシリーズの目的は

最低限アプリとして使えるような構造の
CoreDataの利用ができるようになること。

つまり、
『AppStoreに並べてもいいかも?(無料なら)
ってくらいの”CoreDataを使ったTodoアプリ”を完成させる!』

ってことです。

 
 
しかも、それを僕のような”CoreDataビギナー”がやるってんだから、
そりゃ大変!!(笑)

まぁ、まだまだ実際のTodoアプリ作成には手を付けられていないですが、
ちゃんと目標を見据えて地道にやって行くことにしましょうね。

ってことで『Study CoreData』第5回です!

注意:投稿者自身もCoraDataについて勉強中のため、このシリーズには誤りが含まれている可能性があります。もし、間違いに気付かれた方はコメント欄もしくはtwitterなどでご指摘いただけると幸いです<(_ _)>
また、開発環境はXcode3.2.3 iPhone SDK 4です。実機でのテストなどは自己責任でお願いいたします。

 
 
今回はちょこっとずつコードやデータモデルをいじって、アプリの表示を変えるところまでやってみます。

んぢゃ、れっつえんじょいでべろっぴん♪
(では、始めましょう!)
 
 
第3回で、Todoアプリのためのエンティティを設計したので、それを使っていきたいところなんですが、
やはりそこにいきなり手を付けるのはデンジャラスなので、元からある<Event>エンティティにちょっとずつ手を加えて、
Todoアプリを作るにあたって必要な知識を身に付けていくとしましょう。

そのためにまず、データモデルのバージョンを初期状態に戻す作業をしておきます。
 

1. データモデルのバージョンダウン

 
以前にも触れましたが、こんな感じです。
<グループとファイル>ペイン<Resources>にある古いデータモデルファイル<TodoCore2.xcdatamodel>を選択した状態で、
メニューバーの<設計><データモデル><現在のバージョンを設定>を選びます。

そして、下のように選択したデータモデルファイルに緑のマークが移動していればOKです。

これで、ビルド時に読み込むデータモデルファイルは<TodoCore2.xcdatamodel>になりました。
 
 
では、次は何をしましょうか?

初期状態でテーブルビューの各セルに表示されるのは、<Event>エンティティの唯一の属性である<timeStamp>だけでしたよね。
ですが、Todoアプリを作るのであれば、ひとつのセルに、
Todoのタイトル、重要度、締め切り日くらいは表示させたいですよね。

なので、その練習としてセルに表示する属性を追加してみることにしましょう。
 
その属性名は<title>としましょう。
え〜と、他には
「テーブルビューのセクションにあたる属性も設定できる」
って以前に書きましたよね。
ちょっとそれも試してみたいので、さらに<sectionNumber>という属性も加えてみますね。
 
 

2. 属性の追加(おさらい)

それぞれの属性の指定は以下のようにしておきます。

属性名<title>
オプション:OFF、データ型:文字列、最小長:1、デフォルト値:No title
属性名<sectionNumber>
オプション:OFF、データ型:整数16、最小長:0、最大長:3、デフォルト値:0

 
で、こんな感じになるかと。

念のためここでの設定を説明しておきますね。
 
 
まず、<title>属性には文字列が入ります。これは必須項目ですが値が何も入れられなかった場合、[No Title]という文字列が自動的に入れられます。
次の<sectionNumber>には0〜3までの整数値(NSNumberでラップされたもの)が入ります。この属性もデフォルト値があるので、値を入れなければ0が挿入されます。

まぁ、説明するまでもなかったかもですね^^;
これに<関連>プロパティも加えたいところですが、そこまでやると混乱しそうなので(僕が…)それはまた後にします。

それでは、いよいよコードの変更をしにいきましょう!
 
 

3. データのソート条件の追加とテーブルビューのセクションへの対応

…って言っても、どこから手をつければいいのやら…(>〜<;)

そう言えば、テーブルビューのための便利ツール
<NSFetchdResultsController>のインスタンスを作る時に、エンティティの名前とか入れたり、ソートの達人がtimeStamp持ってたりしてましたよね。
そこでたしか、セクションに対応する属性も設定できたはずです!
 
 
んぢゃ、そこからいきましょう!
RootViewController.mの
[- (NSFetchedResultsController *)fetchedResultsController]内です。

その中の

NSSortDescriptor *sortDescriptor = 
     [[NSSortDescriptor alloc] initWithKey:@"timeStamp" ascending:NO];

NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
    
[fetchRequest setSortDescriptors:sortDescriptors];

NSFetchedResultsController *aFetchedResultsController = 
     [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest 
                  managedObjectContext:self.managedObjectContext
                    sectionNameKeyPath:nil cacheName:@"Root"];

をいじっていきましゅ。
 
 
まず<sortDescriptor>のインスタンスメソッドの最後の引数<ascending:NO>を<YES>に変えて古い順に表示されるようにしてみましょう。
で、もうひとつ<NSSortDescriptor>のインスタンスを加えて『<title>属性を降順に』という設定にして、<SortDescriptors>に加えてみてください。
 
 
どうです?
できたでしょうか?

こんな感じになっているかと。

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] 
     initWithKey:@"timeStamp" ascending:YES];

NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc] 
     initWithKey:@"title" ascending:NO];

NSArray *sortDescriptors = [[NSArray alloc] 
     initWithObjects:sortDescriptor, sortDescriptor2, nil];

[fetchRequest setSortDescriptors:sortDescriptors];

(※新しく用意した<sortDescriptor2>はあとでreleaseしてくださいね。)
 
ただ、これだとソート条件を追加する度にreleaseも書かなきゃいけないので、ちとメンドイですよねぇ。
実はこんな風にもできちゃいます!

NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:
     [NSSortDescriptor sortDescriptorWithKey:@"timeStamp" 
                                                  ascending:YES], 
     [NSSortDescriptor sortDescriptorWithKey:@"title" 
                                                  ascending:NO], nil];

[fetchRequest setSortDescriptors:sortDescriptors];

この[sortDescriptorWithKey: ascending:]は[autorelease]されるインスタンスメソッドなので、いちいち[sortDescriptor release]を書かなくても良くなります。
 
ですが、これはiPhone OS4.0 で追加されたものなので、アプリが対応するOSのバージョンによって検討してください。
(※この新しいメソッドを使う場合は、[sortDescriptor release]の記述を削除します。)

 
 
これで、新たなソート条件は設定できました。
次はセクションに対応させる属性ですね。

これは簡単です。
[sectionNameKeyPath:]の引数に属性名を文字列で入れるだけです。

NSFetchedResultsController *aFetchedResultsController = 
     [[NSFetchedResultsController alloc] 
          initWithFetchRequest:fetchRequest 
          managedObjectContext:self.managedObjectContext 
            sectionNameKeyPath:@"sectionNumber" 
                     cacheName:@"Root"];

さっ、ここまで出来たら次へ行きましょう!
 
 
お次ぎは、新規データを作成して値を設定するところ。
もうお分かりですね。

そう![insertNewObject]です。
 
 

4. 最後のメソッドを解読する

そこに<sectionNumber>属性への代入も入れようと思います。
(ひとまず<title>属性の値は設定せず、自動で入れられるデフォルト値のままにしておきます。)
 
 
にゅーまなねーじどおぶじぇくとに、せっとばりゅーふぉーきーして…
きーが@”sectionNumber”で、ばりゅーが…

…あれ!?
 
 
そう、<sectionNumber>のvalue(値)に何を設定したらいいのか決めてませんでした。
(これをデフォルト値(defaultValue)にする事も出来ますが、常にデフォルトの0が入れられるだけだといくらデータを増やしてもセクションはひとつってコトになっちゃいます)

とりあえず、ここでは
『rowが3つめになったら新しいsectionをつくる』
って感じにしたいなぁと思います。
 
 
すごく単純に考えると、こんな感じのメソッドが必要になるかと…

#define MAX_COUNT_ROW 2

- (NSUInteger)newSectionNumber {

     NSUInteger sectionCount = xxxxx;
     NSUInteger rowCount = xxxxx;

     if (rowCount <= MAX_COUNT_ROW)
          rowCount++;

     if (rowCount == MAX_COUNT_ROW)
          sectionCount++;

     return sectionCount;
}

(かなり大雑把ですがテスト用なので<(_ _;)>)

ってことで、現在のセクション数セクションにあるrowの数が必要になりそうです。
 

それらを取得する方法を知るために見るのが、
ついに自動生成されたメソッドの中の最後になります!!!

それは、皆さんご存知の[numberOfSectionsInTableView:][tableView: numberOfRowsInSection:]です。
知っての通りそれぞれ『セクション数を返す』メソッドと『セクションの行数(row)を返す』メソッドなので、この中に答えがあるはず。

セクション数の取得はめっちゃ簡単ですね!
 
<NSFetchedResultsController>がセクションに対応する属性を知っているので、その数をカウントすればいいだけ。
楽チンです♬
 
 
お次の行数はというと…

例のセクションズにセクションの番号を渡すと<NSFetchedResultsSectionInfo>というのが返ってくるので、それに[numberOfObjects]すると、セクションの行数が返されるみたい。

まぁ、仕組みは簡単そうですが、謎の<NSFetchedResultsSectionInfo>ってのが出てきましたね。

そこで必殺!! ⌘+ダブルクリックですっ!

そう言えば< >で囲われてましたが、プロトコルになっていますね。

プロトコルってのを簡単に言うと、メソッドを固めたもの。
つまり、クラスのようにインスタンス化してどーのこーのって言うんぢゃなくて、
限定したクラスにしばられずに使えるメソッドを集めたものがプロトコル
って呼ばれるみたいです。
ちなみにデリゲートもプロトコルですね。
(これ以上詳しい説明をすると間違ってそうなので、そんな感じってことで^^;)
 
 
つまり、さっきの

[[self.fetchedResultsController sections] objectAtIndex:section]

で返されたものは<NSFetchedResultsSectionInfo>プロトコルに対応したものになっていて、
そこから取り出せるのが、

<セクション名>:(NSString)
<セクションのインデックスタイトル>:(NSString)
<セクション内のオブジェクトの数>:(NSUInteger)
<セクション内のすべてのオブジェクト>:(NSArray)

なんですね。
特定されたクラスのものではないので<id型>になっていたみたいですね。
 
 
ちょっとややこしいですが、とりあえずこの2つを使えば行けそうですね!

こんな感じにしてみました。

#define MAX_COUNT_ROW 2

- (NSUInteger)newSectionNumber {

     NSUInteger sectionCount = 
         [[self.fetchedResultsController sections] count];

     if (!sectionCount)
          return 0;

     sectionCount--;

     id <NSFetchedResultsSectionInfo> sectionInfo = 
          [[self.fetchedResultsController sections] objectAtIndex:sectionCount];

     NSUInteger rowCount = [sectionInfo numberOfObjects];

     if (rowCount < MAX_COUNT_ROW + 1)
          rowCount++;
     
     if (rowCount >= MAX_COUNT_ROW + 1)
          sectionCount++;
     
     return sectionCount;
}

まぁ、全体的な内容はかなりいい加減なので、重要なところだけ。
(どれだけいい加減なのかは、あとで実証しますw)

 
 
まず、セクション数の取得は[numberOfSectionsInTableView:]からそのままパクるとして^^;、そのあとで

if (!sectionCount)
     return 0;

としてるのは、一番始めのオブジェクトを作る時にはセクション数が0なので、そのまま返させるためです。
これを消すと落ちます。
 
 
そのあとにsectionCountから1引いているのは、次の[objectAtIndex:]に合わせるためです。
あとは[tableView: numberOfRowsInSection:]をまんまパクってrowCountを取り出します。

で、ごにょごにょしてsectionCountを返します。
 
 
では、コイツを使って新規オブジェクトに<sectionNumber>の値を代入してみてくださいね。
 

5. データに新たな値をつっこむ!

どうでしょう?
できましたでしょうか??

こんな感じになってますかね。

<sectionNumber>の値を入れる際に<NSNumber>に変換するのをお忘れなく!
 
 
で、次はセクションヘッダの表示を追加します。

#pragma mark Table view data sourceブロックのあたりに、[tableView: titleForHeaderInSection:]メソッドを追加して、<NSFetchedResultsSectionInfo>を取得し、さらにそこから<name>を取り出します。
 
こんな感じになればOK!

では次っ!!
さっさと行きます。
 
 

6. セルの表示を変更してみる

あとはセルの表示ですね。
これは確か[configureCell: atIndexPath:]でやってましたね。

ちなみになぜ[cellForRowAtIndexPath:]に直接書いていないのか?と言うと、
このメソッドは[cellForRowAtIndexPath:]以外にも、例の勝手に呼ばれるメソッド群の中の[…didChangeObject:…]内で<NSFetchedResultsChangeUpdate>時の処理としても呼ばれているからです。
 
つまり、データがアップデートされた時とセルが表示される時で同じ処理をしているんですね。
 
 
では、その[cellForRowAtIndexPath:]で複数の値が表示されるように変更しましょう。
 
まず、timeStampを文字列化した値を表示させている<cell.textLabel.text>には、<title>の値を。
次に<cell.detailTextLabel.text>を加えて、そっちに<timeStamp>の値を表示させるようにしましょう。
ただ、そんなに細かい情報はいらないので<NSDateFormatter>を使って西暦と月日だけにしましょうか。

じゃ、やってみてくださいね。
 
 
僕は、こんな感じにしてみました。

※日時の表示に使っている[localizedStringFromDate: dateStyle: timeStyle:]は、これまたOS4.0で追加されたものですので、実際のアプリで4.0未満に対応させたい場合は別の方法にしてください。

と、ここまで出来たら後は、cellのタイプを変えれば準備完了です!
[cellForRowAtIndexPath:]内の<UITableViewCell>の初期化メソッドでセルのスタイルを<UITableViewCellStyleSubtitle>に変えてください。
 
 

7. ビルドとエラー

では、一旦シミュレータ内のTodoCoreアプリを削除して、

ビルド実行っ!!

このように動いてますでしょうか?
 
追加ボタンを押してみると分かるように、デフォルト値が設定された属性(titile)は,
何も指定しなければ勝手にデフォルト値にしてくれるんですね。
 
 
ただ、このプロジェクトにはいくつか問題がありまして…

A. セクション数が3を超えようとするとアプリが強制終了する。
B. 作られたセクションを削除すると、それ以降に作られる<sectionNumber>がおかしくなる。

他にもあるかもですが、分かりやすいのはこの2つですね。
 
Bのバグは僕がつくった[newSectionNumber]メソッドがいい加減だからです。
本来であれば、全セクションのrowの数を取得して、それぞれの場合の処理を決めなければいけないのですが、
めんどくさいですし^^;、何より今回のテストでそこまで考えるのはムダなのでしませんでした…<(_ _)>

で、Aのバグですが、この原因は、もうお分かりの方もいらっしゃると思いますが、
<sectionNumber>属性の最大長が3に設定されているからです。
 

つまり
<属性>の設定にそぐわない値が代入された場合、
何もしなければアプリが強制終了される

事が分かりますね。

こういった場合では、エラーを出さないために値が代入される前の時点でその値が適切かどうかを調べて、適切でない場合は何らかの対処をしておく事が大切になりそうです。
(もっと確実なのは、絶対に適切な値しか入らない作りにしておくとか。)
 
 
例えば今回の場合なら、
[newSectionNumber]のreturn前に4以上であれば、別の数を返す
とか、
アラートを表示してユーザにセクションを消しても良いか聞く
とかですね。

まぁ、CoreDataに限らず、エラー/バグがあった場合、どういった状況でエラーが出るのか、またどうやって防ぐのかってことを、常に意識しながら開発していくのは大切ですよね。
(言ってる僕もまだまだですが…^^;)
 
 
と、そんなこんなで今回はお開きです。
バグはほっときます(笑)
 
 
次回はさらに関連を追加して表示、そしてちゃんとタイトルを入れられる仕組み作りに突入します!
 
 
いや〜ついに6日目もこれで終わりますが、もうすぐ一週間…。
なのにまだ、デフォルトをちょっといじった程度です。

気が遠くなってきた方もいるかもしれませんね〜
 
 
でも大丈夫っ!!

僕はその倍以上かかってますからっっ!(^〜^;)

その分このシリーズに詰め込んでるワケなので、なんとかこのシリーズを消化してくれた方々には、
『やってみてよかった!』って言ってもらえるようにがんばってます。

たぶん、このシリーズが完結するところまで理解できれば、もっと難しい書籍から学んだり応用なども出来るようになるんぢゃないかなぁと思いますんで、
ゆる〜くがんばって行きましょうね!

それでは、また明日!

Study CoreData 5 ~まだまだ序盤~」への8件のフィードバック

  1. ピンバック: [タモリ] 無料で読めるWeb漫画が気になる。

  2. こんにちは、こちらのサイトで楽しく勉強させてもらっております。
    coredataの資料は本当に少ないもので、こういう記事は本当に助かります!

    ところで、本文中程にあります[sectionNameKeyPath:]の引数についてですが。
    cacheName:@”Root”];ではなく、
    cacheName:@”Event”];ではありませんか?

    宜しければご確認していただければと思います。

    • (‘A`)さん初めまして!
      コメントありがとうございます。

      念のため、初めからプロジェクトを作り直して確認してみました。
      質問されているのは、恐らく[- (NSFetchedResultsController *)fetchedResultsController]メソッドの部分だと思うのですが、
      僕の環境ではここまでの手順でやっていった場合、記事内容と同じようにcacheNameは@”Root”になっています。
      (多分ViewControllerの頭の名前と同じになるように自動で設定されるようになっているのだと思います。)

      ただ、cacheNameはフォッチドリクエストを再利用するときのためのキャッシュIDのようなものなので、@”Root”でなくても問題はないはずです。
      なのでそれほど気にしなくても良いかもです^^;
      (それまでのキャッシュを利用したくない場合は、この引数を変えたりnilを入れたりできますね。)

      このシリーズも注意書きにあるように100%完璧な情報とは言えないと思うので、また何かあれば気軽にコメント頂けると助かります

  3. 初めまして
    初心者で参考にしながら勉強しています><

    最後の[cellForRowAtIndexPath:]の部分を入力してビルド実行すると、- (void)configureCell:〜の行に、expected ‘;’ before ‘{‘ token と出てしまいます…
    抜けている部分はないはずなのですが何がいけないのでしょうか…><?

    • 10rさん、はじめまして!
      コメントありがとうございます<(_ _)>

      “expected ‘;’ before ‘{‘ token”はよく出るエラーですね。
      日本語だと『”{“の前に”;”が必要なはずです』ってな感じかと。

      恐らく、エラーが出ている行にある”{“の前(もしくはその前の行辺り)で”;”が抜けてしまっている所があると思いますのでチェックしてみてくださいね。

  4. 初めまして。これからCoreDataを勉強するところで、参考にさせていただいています。
    7でビルド実行すると、新しいテーブルが追加できず落ちてしまいました。
    insertNewObjectの実装部分に”[newManagedObject setValue:@”No Title” forKey:@”title”];”を追加すると解決しました。
    という報告です(^^;

    • 翔太さん、初めまして!
      コメントありがとうございます。

      ご指摘の件ですが、もう一度『2. 属性の追加(おさらい)』の部分を見てください。
      記述通りtitle属性のデフォルト値に”No Title”を設定してあれば、自動的に入れられるのでinsertObject部分への追記は不要です。

  5. ピンバック: [Xcode4] CoreData のxcdatamodel ファイルバージョンを変更する方法 | 極上の人生

jacminik への返信 コメントをキャンセル