Study CoreData 9 ~こだわり続けるのは大変だ~

今日もしぶとく更新していきます。『Study CoreData』。
ついに明日で連続更新記録二桁目に突入です!

第0回から参加してくれている方はモチロン
途中からでも見てくださっているみなさんには本当に感謝してます!

ありがとうございます!!

少しずつでもCoreDataが使えそうな気になってくれてると良いのですが、どうでしょうか?


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

 
 
先に断っておきますが、
前回までで、自動生成されたコードからのカスタムはほぼ終わっています。
なので今回はCoreDataに関する部分はかなり少ないです。
(次回からはやっと、当初からの目標であるTodoアプリに取りかかります!)

前回の最後で『ユーザビリティ』を考慮したブラッシュアップを…
なんてコトを書きましたが、今回の作業に入る前に良いきっかけですので

『開発を始めて一年間、
TapDaysを作るにあたって僕が考えたユーザビリティについて』

ちょこっと能書きを書かせて頂きたいなと。
(つい先日、IDPの初更新をしました)

もし何かの参考になれば幸いです。
 
 

1. iPhoneアプリでのユーザビリティについての私的考察

今回このシリーズの題材となっているような
『ユーザがデータを入力して使うアプリ』(Todoやメモ、スケジュールなど)の場合、特にまず考えるべきは
『いかに簡単に入力させるか?』
ってコトだと思います。

まぁ、こういったアプリを使ってユーザが主にする行為が
『データを入れる』ことと
『データを見る』ことなので、当たり前っちゃぁ当たり前ですね。
 
 
そして、実際に多く見かけるデータ入力系のアプリで(特に高機能なもの)の実装は、以下のようになっていることが多い気がします。

1. (ユーザ)追加ボタンを押す。[tap:1]
2. 入れられるデータの詳細が表示される。
3. (ユーザ)どのデータを入れるのか選択。[tap:2]
4. (ユーザ)データを入力して保存。[tap:3]
5. 詳細ビューに戻る。
6. (ユーザ)他は特に編集しないなら保存ボタンを押す。[tap:4]
7. リストに戻る。(データ完成)

 
と、ひとつのデータ(例えばTodoひとつ)を完成させるために必要な最少タップ数は4回
(Appleのサンプルコード『CoreDataBooks』もこの作りですね)

でも、これ(タップ数)ってちょっと多い気がしません?
 
 
で、僕はこうなってたらいいのに。って思います。

1. (ユーザ)追加ボタンを押す。[tap:1]
2. 必須データ(Todoならタイトル)の入力画面へ移動。
(この時の画面には、[保存ボタン]と[さらに詳細を入力]ボタンが表示されている。)
3. (ユーザ)データを入力して、特に詳細情報を追加しないなら保存ボタンを押す。[tap:2]
4. リストに戻る。(データ完成)

 
どうでしょうか?こうすると最低限必要なタップ数は2回に減ります。
(例えば5つのタスクを入力するのに前者なら20タップ必要ですが、後者ならその半分で済みます)
 
 
すごく細かいことかもしれませんが、こういったちっちゃなことが
『ユーザに快適さを与える』ひとつの要因
になるんぢゃないかと思うんですよね。
(ユーザははっきりとは気付かないコトが多いと思いますが、それを感じるんぢゃないかと)

つまりアプリを作る上で、デザインや機能はもちろん大切ですが、それ以上に『もっと使いやすく』ってコトを常に考えながら進めていくことはもっと重要だよなぁって思ってます。
(そのことが、競合するアプリとの差別化にもなるかもですし。)

だってどうせつくるなら、

『似たようなアプリは他にもあるけど、
なんかこのアプリが一番使いやすいんだよねぇ』

って思ってもらいたいぢゃないですか(^〜^)

…と、そんな感じです。
えらそーにノウガキ垂れました^^;
 
(リリースしたこともないくせにえらそーにっ!)
 
(はいスンマセン<(_ _)> …ちょーしにのりますた(笑))

ちょっと前置きが長過ぎましたね。その辺も考慮しながら本題に入ります。

今回は、Jacminik流ユーザビリティの実装
しばしお付き合いくださいませ<(_ _)>
 
 

2. [More Details]ボタンを追加する。

まずは、どこにボタンを設置するかですが、いくつか考えられますね。

A. ナビバーの[Save]ボタンを[UISegmentedControl]に変更して、そこに
[Save]と[More Details]を設置。

B. 編集/追加作業を行うView内のどこかに設置。

 
他にもあるとは思いますが、
分かりやすさという点ではAがベストかな?と思います。
ただ、セグメントにするには画像(ボタンアイコン)を用意してあげないと
“More Details”って文字列では長過ぎますよね。
なので、手抜きではありますが今回はBでいくことにさせてください<(_ _;)>

そんな理由もあって今回はテーブルビューの[セクションフッタ]にこのボタンを設置してみます。

[isFirstEdited]で分岐して、
新規追加の時は”さらに詳細へ”ボタン
編集時には”保存してリストに戻る”ボタンとしてみました。
(ボタンのタイトルはもっと良い言葉がありそうですが思い浮かばないので…<(_ _;)>)

※ハイライトしている[buttonWithType:102]というのは隠しAPIで、標準の[Saveボタン]と同じ外観にしています。
かの有名なLCD ClockやMyWebClipなどの開発者さんのブログを参考にさせて頂きました。<(_ _)>
そのエントリはこちら。

UIButton の隠し API – 24/7 twenty-four seven
 
 
で、これでボタンの表示はOKですので、あとは今[nil]になっているボタンのActionを用意しましょう。
 
 

3. 頭で考えると分からなくなりそうな時は、とりあえず図にしてみる!

そこで、まずは各状況下でどのような画面遷移をさせるのかを考えます。
ただ、言葉だけで考えるにはちょっと複雑になりそうなので
以下のように簡単な図を使って考えてみました。

(テキトーかつ汚くてスミマセン…)

ん〜…なかなか手強い感じですねぇ。
同じ[Save/Cancel]ボタンや[Save & Show List]ボタンなどを押したとしても、
それ以前の画面遷移によって違う処理をさせなければいけなくなったりと、
ユーザビリティ改善のためとは言え、たったひとつのボタンを追加するだけで
これだけの状況に対処する必要が出てくるワケですね。

ただ、これを実装するのとしないのとでは
やはり多少なりとも違ってくると思いますので、
がんばって実装することにしましょう!
 
 

4. デリゲートは常にRootViewController

まず、保存/キャンセル処理などをさせるためのデリゲートですが、
これは常に[RootViewController]になるようにしなければいけませんね。

ですが、ここで以前書いたコードに問題が出てきます。
その問題のコードが[DetailViewController]のコレです。

ボタンを追加する前の段階ではこれで良かったのですが、上の遷移図を見直してみると分かるように、このままでは
デリゲートが[RootViewController]ではなく[EditableViewController]になってしまう場合も出てきます。

その対策としては、
[DetailViewController]にも[id delegate]プロパティを持たせて、
それをRootViewとEditableViewの橋渡しとして使えば良いのではないかと。

つまり、

[Root]内で[Detail]を初期化する際には、
detailViewController.delegate = self; とし、

[Detail]内で[Editable]を初期化する際には、
editableViewController.delegate = self.delegate;

[Editable]内で[Detail]を初期化する際には、
detailViewController.delegate = self.delegate;

とするなど、
とにかく常にデリゲートが[RootViewController]になるように変更します。
(いくつか変更箇所がありますのでプロパティを追加して、すべて変更してください。)
 
 
それでは次に、新しいボタンに対応させるための実装に入ります。
(これから示すのは一例ですので、もっとスマートでベターな方法があればその方法をとった方が良いと思います。…というのも、自分としてももうちょっとスマートな処理にできないもんかなぁ、と思うもので^^;)

で、僕が考えたのは…
 
 

5. 新しいボタンタグとプロパティの追加。

まず、新しいボタンのためのtag#defineで用意してボタンにセットします。
そしてそれを、他のクラスからも参照できるようにヘッダファイルへ移動して、
さらに[ボタンアクション]が呼び出された時に
『保存後編集を続けるのかどうか?』を示すためのフラグ(プロパティ)も追加します。

(EditableViewController.m)

そして、
新しいボタンのタグ[TAG_SUB_BUTTON][continueEditing]プロパティを利用して、
[didEditManagedObject:]を以下のように書き換えます。

ここでの処理は、遷移図と照らし合わせてもらうと分かりやすいと思います。
[continueEditing]というのは、簡単に説明すると
『遷移後の画面で編集を続けるかどうか?』というフラグとして使っています。

なので、
新規イベント作成時に[More Details]ボタンが押された時
と、
詳細ビューから遷移した後の編集ビューで[Cancel/Save]ボタンが押された時
だけ、YESを返します。
逆に言うと、それに当てはまらない場合は『リストビューに戻る』ってコトになります。
 
 

6. 最後の変更。

で、次に変更するのは、
ココから呼ばれる[RootViewController]の[didEditWithViewController: saved:]です。

ちょっとココは変更箇所が大きいのでメソッド全体は載せませんが、コンテキストに保存するところまでは以前のままです。

なので、ハイライトしてある部分だけを変更/追加すればOKです。

ちょっと長いですが、実際は詳細ビューを初期化してPushするためのコードがほとんどなので、分岐処理自体は4つだけです。
 
 
上から説明していくと…

まず、先程追加した[continueEditing]を取得して、それがYESの場合の処理をします。

この時に[isFirstEdit]なら、[新規イベントビュー]で[More Details]が押された後なので、詳細ビューを初期化してPushさせます。
で、そのビューの中でキャンセル/保存が出来るようにボタンを追加して、ナビゲーションバーの[BackButton]を表示させないようにします。

そして[isFirstEdit]がNOなら、[新規イベントビュー]以外で[Save/Cancel]ボタンが押された後なので、controllerをPopします。

つぎは編集を続けない(終了する)際の処理になります。
ここでは、そのコントーラのナビコントローラの[ルートコントローラ]がRootViewControllerかどうかで分岐しています。
(ちょっと分かりにくい言い回しでごめんなさい^^;)
つまり、モーダルビューからPushしてきたのか?RootViewControllerからPushしてきたのか?ってコトです。

そして、それがRootViewControllerからなら、
[リストビュー]でセルが選択されて、[詳細ビュー]から遷移した[編集ビュー]内の[Save & Show List]が押された時の処理になるので、一気に[リストビュー]へ戻ります。

で、残りは[新規イベントビュー]で[Save/Cancel]ボタンが押された時か、[新規イベントビュー]から遷移した[編集ビュー]内の[Save & Show List]が押された時なので、dismissModalViewします。

…と、こんな感じですが、頭がゴチャゴチャになりそ〜ですね(^〜^;)

でもまぁ、こういうのは自力で考えてる時は何となく分かるものなので、むりくり理解する必要もないかもです。
 
 
あと最後にもうひとつ。
(これはバグがあったので追加しました。)

同じメソッド内の[if (isFirstEdited)]のところに
“今ビジブルなコントローラがEditableViewの時だけ”って条件を加えます。
これを入れておかないと詳細ビューから保存して戻る時に、せっかく入力したものが新規保存の時と同じ値に変更されて保存されてしまいます。
 
 

7. 体験してみる。

では、ここまでで実装は完了したので、一度シミュレータ内のアプリを削除して
まっさらな状態からビルドして色々と試してみてください。
 
 
それぞれの保存時にちゃんと保存されているか?

[新規イベントビュー]からイロイロ入力していって
[キャンセル]した時にどうなるか?

…などなど。
 
 
まだまだ完璧とは言えませんが、

『前回までと比べて使いやすくなった。』
『編集と確認が楽になった。』

と感じてもらえたら幸いです(^〜^)
 
そして、色々と試し終わったら
僕らが書いてきたすべてのコードをさらっと眺めてみましょう。
 
 
すると、ここまで色々とコードを追加/変更し、
さらにユーザビリティまで考えてやってきた割には
随分とあっさりしてるなぁと感じるんじゃないでしょうか?
僕はそう感じました。
 
実際、デフォルトの状態から追加したクラスはふたつだけですし。
 
 
たぶん、これこそが
CoreDataをAppleが推奨する要因のひとつなんじゃないかと。
つまり、CoreDataを使わずにデータベースを扱う場合よりも書くコードが少なくて済むってこと。

あらためて『こりゃ使うっきゃないな』なんて思っちゃうわけです。
 
 
…と、そんなこんなで、10回に渡ってお付き合い頂いた『Study CoreData』も今日でおしまい

ぢゃないですよっ!!

ちゃんと最後までやりますので、ご心配なく(^〜^:)
 
 
でも真面目な話
ここまでちゃんと読んでくれた方々、本当にお疲れさまです!

ちょっと今回は分かりにくい点が多かったかもしれませんね。
次回からはもうちょっと分かりやすく出来たらと反省しております。

では、次回はいよいよTodoアプリ開発本格始動です!!

おたのしみに〜♪

それでは、また明日!
 

Study CoreData 9 ~こだわり続けるのは大変だ~」への8件のフィードバック

  1. ピンバック: [朝刊] アップル, 先日の記者会見動画を Youtube に公開。

  2. ピンバック: 頭と尻尾はくれてやる!

  3. 初めまして。
    私は開発を開始してまだ日が浅いのですが、CoreDataに挑戦したいと考えております。
    こちらはとてもわかりやすくCoreDataの説明や便利機能などが記載されているので、とても勉強がしやすく助かっております。

    現在は自分が作りたいと思っているものを制作しているのですが、壁にぶつかってしまっています。

    アプリの内容は
    起動→TableView(ユーザ登録画面)→ユーザ登録がされているCellをタッチするとユーザの詳細画面(TableView)→詳細画面のCellをタッチするとユーザが好きなようにカテゴリを登録できる画面
    というようなものを作りたいと考えています。

    しかし、詳細画面のCellをタッチしたあとのカテゴリ登録画面が全ユーザが同じカテゴリになってしまいます。
    ユーザごとにカテゴリを作るようにするにはどうすれば良いのでしょうか。

    • 初めまして!
      コメントありがとうございまっす<(_ _)>

      御質問の件ですが、ちょっと僕の方でアプリの構造が理解できていないので質問させてください。

      同じアプリ内で複数のユーザアカウントのようなものを作れるという事でしょうか?
      それと、
      >全ユーザが同じカテゴリになってしまいます。
      というのは、登録したはずのカテゴリがデフォルト状態に戻ってしまう。もしくは、ひとりが登録するとすべてのユーザに反映されてしまうということでしょうか?

      理解が違っていたらすみません。。

  4. 返信ありがとうございます。
    こちらこそ説明不足で申し訳ありません。

    私が作りたいのは、複数のユーザアカウントをつくり、そのユーザそれぞれがカテゴリを追加削除ができ、作ったカテゴリの中身もそれぞれのユーザが好きなように追加、削除ができるようなものです。

    例えばユーザを食べ物、歌手として考えると、
    食べ物→食べ物の説明→食べ物の一覧(丼、寿司等)→丼をタッチすると丼の種類が一覧表示され、新たに発見した丼を追加できる。

    歌手→ 歌手の説明 → 歌手一覧 → 一覧の中の1人をタッチするとその歌手がリリースした曲名が表示され、新たにリリースしたら追加できる。

    といったような感じにしたいと思っています。
    伝わりましたでしょうか。

    全ユーザが同じカテゴリになってしまうというのは、1つカテゴリを登録すると、すべてのユーザに同じカテゴリが反映されてしまうという事です。

    • なるほど!ありがとうございます。理解できました!

      ただ、この情報だけでは具体的にどこを変更すればいいのか限定するのは難しいのですが、簡単に考えるとモデリングは以下のようになるのかなと。


      (※ブルーでハイライトしている部分が実際に使う構造です。ピンクのエンティティは説明用)
      上のように、まずすべてのアカウントを取りまとめる大もとのエンティティ(UserData)をつくり、各アカウント(Account/食べ物や音楽)との関連を一対多関連(OneToManyRelationship)にします。
      次に[Account]から多対多(ManyToMany)な関連をもつエンティティ(Category/丼、歌手名など)を用意します。
      (ManyToManyにしたのは、他のAccountからも同じCategoryが選択できるようにするためです。)

      こうするとひとつのAccountからアクセスできるCategoryは、Accountと関連づけられているものだけになるので、他のAccountにまで反映されてしまうことはないと思います。

      僕の方で分かる範囲でかなり単純に考えてみたのですが、こんな感じでどうでしょうか?

      あと実際のエンティティ名は、MainData(今のUserData)、Category(今のAccount)、Item(今のCategory)とした方が分かりやすいかもしれませんね。

  5. 返信ありがとうございます。
    まず、モデリングの時点で考え方が違っていたようです。
    教えて頂いたモデリングで一度作り直してみたいと思います。

    もし、また壁にぶつかってしまったらコメントしますので、よろしくお願いします。

    • 単純な解答でしたが、お力になれたならウレシいです。

      モデリングは実際のデータ構造によって変わってくるので、先に説明した方法がベストだとは限らないと思います。
      なので、色々やってみて試してみるのがいいかもしれませんね。

      今後もお互いがんばりましょう!
      (今自分も丁度CoreDataでハマってしまっていますが。。^^;)

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