mixi-inc / iostraining-todo-app Goto Github PK
View Code? Open in Web Editor NEWiOS Training で使う教材用TODOアプリのリポジトリです
License: Apache License 2.0
iOS Training で使う教材用TODOアプリのリポジトリです
License: Apache License 2.0
TODOアプリを作ってみようシリーズの第3回目の演習課題その2です。
#3 に引き続き、この回ではTODOの永続化を行います。
ブランチ write-to-userdefauls にチェックアウトすると回答を見ることができます。
永続化の仕組みにはローカルDB、ファイルシステム、UserDefaults、サーバーに保存などのやり方が幾つかあります。それぞれの方法について一長一短ありますが、ここでは最も手軽に利用することのできる、NSUserDefualtsを利用します。
今まで与えていた初期値を与えないようにします。
UserDefualtsに保存を行うときは、保存したいオブジェクトとユニークなキーを指定して書き込みを行います。(- setObject:forKey:
を用います)
また- setObject:forKey:
を行ってもすぐにはファイルへの書き込みが行われないので、- synchronize
を読んで書き込む必要があります。
NSUserDefaultsの実体はplistファイルとして端末に保存され、アプリの起動時にメモリ上に展開されます。
シミュレータの場合は以下のディレクトリ
/Users/<ユーザー名>/Library/Developer/CoreSimulator/Devices/<シミュレータのUUID>/data/Containers/Data/Application/<アプリケーションのUUID>/Library/Preferences
以下にある
org.punchdrunker.Todo.plist
というファイルがそれに該当します。MacのQuickLookやplutilコマンドでその中身を覗き見ることができます。
viewDidLoad
に以下のようなログを仕込むとログにディレクトリが表示されます。
NSLog(@"%@/Preferences", NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject);
property list (plist)を扱うためのユーティリティコマンドです。plistの中身を出力したい場合は
plutil -convert <json|xml1|binary1> -o - <plistのパス>
で出力することができます。
アプリの起動時にデータを読み込んでセットします。
NSUserDefaultsからの読み込みは - objectForKey:
を用います。
読み込みのタイミングはViewControllerの初期化時が良いでしょう。
#3 で実装した削除を行ったときに、永続化しているデータの更新を行います。
UserDefaultsを更新する場合は、上書きを行えば削除することができます。
うっかり実装漏れになってしまいそうな点ですが、実際のアプリケーションを作る際は要注意ポイントの一つです。
TODOアプリを作ってみようシリーズの最終回の演習課題です。
TODO入力時のバリデーションと単体テストを取り扱います。
前回( #6 )のゴール地点であるブランチlocal-notification からスタートしてください。
ブランチ unit-testを見ると解答例が書いてあります。
まず、入力されたTODOが仕様を満たすかどうかをチェックするメソッドを作ります。
仕様を満たす場合は YES を、満たさない場合は NO を返すようにします。
そしてTODOが正しくない場合はアラートを表示します。
アラートの表示にはUIAlertControllerを用います。使い方は差分のコードを見てください。
リファレンスは UIAlertController Class Reference になります。
このUIAlertControllerはiOS8でクラス名が変更になったクラスで、以前はUIAlertViewを利用していました。
次に単体テストを追加していきます。テストフレームワークであるXCTestがプロジェクト作成時に導入されているのでそれを利用します。
今回テストを行いたい対象はAddTodoViewControllerなのでAddTodoViewController.mのターゲットにTodoTestsを追加します。
次に新規テストファイルを追加します。NewFileからテンプレートは"Test Case Class"を選択し、subclass of はXCTestCaseを選択します。
ファイル名は特に制約はありませんがAddTodoViewControllerTestsなどにすると分かりやすいです。
テストファイルが追加できたらテストターゲットのヘッダファイルをimportしてテストを記述します。
XCTestによるテストの記述方法は教材を参照してください。
テストには、成功するパターンや失敗するパターン、異常な入力などの様々なパターンを網羅するとより安心して開発に望むことができます。
ちなみに 4a49faf では以下のケースに対するケアが漏れていて、テストを実行すると失敗となります。
TODOアプリを作ってみようシリーズの第4回目の演習課題その1です。
#4 に引き続き、この回ではTODOの締切日時を入力できるようにし、一覧でも表示できるようにします。
前回のゴール地点であるブランチ write-to-userdefauls からスタートします。
ブランチ add-deadline にチェックアウトすると完成した様子を見ることができます。
このissueで取り組む改善では以下の仕様を追加します
画面のレギュレーションは以下のようになります。
今回の実装は、デザイン面での修正はもちろんですが、データ構造にも変化があります。
今までのTODOはTODO本文のみを管理していましたが、今回は本文に加えて締切日時も加わります。
データ構造の変化がある場合は様々な箇所を変更していく必要があります。その点を留意して、デザイン面での修正と、データ構造など内部的な修正に分けて修正していきます。
まず、TODO本文と締切日時の両方を扱う方法について検討しましょう。TODOを扱う箇所は現在のところ
があります。それに対して、TODOのデータ構造には以下のようなサンプルが挙げられます。
todoBodyList
とtodoDateList
があるようなイメージですNSDictionary *todo = @{@"body": 本文, @"date": 締切};
のようなイメージ@interface Todo : NSObject
@property NSString *body;
@property NSDate *date
@end
(a) のケースでは NSArrayを重複して管理せねばならず、さらにTODOの要素が増えると様々な箇所で引数を追加したりプロパティを追加していく必要があり、将来的に煩雑になりそうです
(b) のケースでは将来的にTODOの要素が増えても引数を各箇所で増やしたりする必要はありませんが、どのキーにどのような型のオブジェクトが入っているかをObjective-Cでは保証できません。このタイプではUserDefaultsにそのまま保存することができます。
(c) 一番堅牢なデータ構造となりますが、新規クラスを作成する必要があるなど、少し修正する量が大きくなってしまいます。またNSUserDefaultsで読み書きできる形式への変換も行う必要があります。
これらのメリット、デメリットを勘案してどのデータ構造を採用するかを決めます。よく使うデータ構造の場合は(c)を採用するケースが多いと思いますが、今回はあまり大きくないアプリなのできちっと型で縛るメリットはあまり享受できそうにありません。そこで比較的簡易にデータを扱うことのできる (b) のNSDictionary で扱うデータ構造を採用したいと思います。
NSDictionary *todo = @{
@"title": <TODOの本文:NSString>,
@"date": <TODOの締切:NSDate>
};
iOSに限らず、Androidでも考慮しないといけない問題なのですが、端末内に保存しているデータ構造を変更する際はマイグレーションについて考えないといけません。
過去バージョンのデータを新バージョンのデータ構造で使えるようにバージョンアップしていくのがマイグレーションとなります。
今回UserDefaultsに保存しているTODOのデータ構造は
となるので、旧バージョンのデータ構造のまま新バージョンで起動すると以下のようにクラッシュが発生します。
// @[ @"todo1', @"todo2"] のようなNSStringの配列
NSArray *savedTodo = [[NSUserDefaults standardUserDefaults] objectForKey:kSavedToDoUserDefaultsKey];
// 新バージョンではTodoをNSDictionaryで扱おうとするが
// 実態はNSStringの配列. Obj-Cはこの段階ではクラッシュしない
NSDictionary *firstTodo = savedTodo[0];
// unrecognised selector でクラッシュする
NSString *todoBody = firstTodo[@"title"];
このような問題に対処するには、バージョンアップ時に保存しているデータ構造を更新処理を行います。
バージョンアップ時に呼ばれるコールバック関数などはないため、バージョンアップ済みかどうかをチェックして必要ならマイグレーションを行います。
一般的には起動後すぐに実行されるメソッドで記述することが多く、UIApplicationDelegateの- application:didFinishLaunchingWithOptions:
などで実行することが多いです。
マイグレーションに時間のかかる場合は、別途マイグレーション専用の画面を用意してやる必要があります。
このissueではマイグレーションを実際には行いませんが、以下のような実装にすると良いでしょう。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSArray *savedTodo = [[NSUserDefaults standardUserDefaults] objectForKey:@"TODO"];
// 旧バージョンのみマイグレーションを行う.
// その判断は保存されているTODOがNSStringかどうか
if ([savedTodo.firstObject isKindOfClass:[NSString class]]) {
NSMutableArray *newTotoList = [NSMutableArray array];
for (NSString *oldTodo in savedTodo) {
// 旧バージョンのデータ構造を新バージョンに合わせる
// 本文を引き継ぎ、締切は不確定だが現在時刻を一応入れておく
NSDictionary *newTodo = @{@"body": oldTodo,
@"date": [NSDate date]};
[newTotoList addObject:newTodo];
}
// 上書きする
[[NSUserDefaults standardUserDefaults] setObject:newTotoList forKey:@"TODO"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
return YES;
}
まずは新規追加画面で締切日時を追加するためのパーツを追加してレイアウトを調整します。
日付の入力にはUIDatePickerを利用します。 UIDatePickerは時間を指定するためのコンポーネントでUIKitに含まれています。UIDatePickerは時間の選択だけですが親クラスのUIPickerViewを利用すると任意のデータを選択することができます。
このUIDatePickerをstoryboard上に追加し、Autolayoutの調整を行います。このUIDatePickerの高さは固定で162ptとなっています。
配置が完了したらこのUIDatePickerをAddTodoViewControllerのプロパティとして追加します。
キーボードが出てきた時にPickerが隠れないようにすることを忘れないでください。
ToDoをNSStringで扱っていた箇所をNSDictionaryで扱うように変更していきます。
以下の該当箇所を直します
- doneButtonTapped:
)- addTodoViewController:addTodoCompleted
)@property (strong, nonatomic) NSMutableArray *todo;
は変えなくてもよいtodo[index]
でアクセスするときに扱う型をNSDictionaryに変える- tableView:heightForRowAtIndexPath:
- tableView:cellForRowAtIndexPath:
またここで、先ほど追加したUIDatePickerから締切日時を取り出しますが、これは datePicker.date
で取り出すことができます。
戻り値はNSDateで、Objective-Cでよく使われる時刻のデータ型です。
TodoTableViewCellに締切を表示するようにします。
TodoTableViewCell.xibに時間表示用のラベルを追加し、レギュレーション通りになるようにレイアウトを調整します。
レイアウトの調整が完了したら、その時間表示用のラベルに時間を代入します。
Objective-Cに限らず、時間を文字列にするには、いろいろな表現方法があります。西暦、和暦、日付のみなのか、秒単位まで表示するのか、タイムゾーンはどうするのか、などさまざまな条件があります。
Objective-CではNSDateFormatter というクラスを用いてNSDateから日付の時刻を取り出します。
リファレンス → NSDateFormatter Class Reference
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = @"MM/dd HH:mm";
NSString *dateString = [dateFormatter stringFromDate:date]; // "05/14 13:00" のような文字列になります
TODOアプリを作ってみようシリーズの発展課題その1 です。
#5 でTODOに締め切りをつけるようにしました。締め切りを過ぎたTODOはタイトルや日時を赤くしましょう。
イメージは上図の date や title の文字を赤くすることです。
TODOアプリを作ってみようシリーズの第3回目の演習課題その1です。
#2 に引き続き、この回ではTODOの削除を行います。
Table View Programming Guide for iOS - Inserting and Deleting Rows and Sections
ブランチ add-todo からブランチを切ってから始めてください。
ブランチ delete-todo にチェックアウトすると回答を見ることができます。
UITableViewにはrowの削除や順番の入れ替えを行うことのできる編集モード(editing)があります。
この機能の一つにセルをスワイプした時に削除ボタンを出すことができるのでそれを利用します。
実装は比較的容易で、UITableViewDataSourceのメソッドをいくつか実装するだけです。
実装するメソッドは以下の二つです。
- tableView:canEditRowAtIndexPath:
- tableView:commitEditingStyle:forRowAtIndexPath:
- tableView:canEditRowAtIndexPath:
はすべてのセルで削除できるので常にYESを返します。
- tableView:commitEditingStyle:forRowAtIndexPath:
では実際の削除動作を行います。
削除は以下のステップで行います。
todoを保持しているデータ構造(現在だとViewController.todo)から削除します。
NSArrayでtodoを保持していると削除できないのでNSMutableArrayで持つようにしてください。
248e9a2
UITableViewからrowを削除するもっとも簡単な方法は [tableView reloadData];
を行うことですが、この場合はアニメーションが追加されません。
動的な更新を行う場合は、[tableView beginUpdates];
と [tableView endUpdates];
の間で更新処理を行います。
tableViewから行を削除するには - deleteRowsAtIndexPaths:withRowAnimation:
を実行します。
TODOアプリを作ってみようシリーズの第4回目の演習課題その2です。
#5 に引き続き、この回ではTODOの締切日時になったら通知を出すようにします。
UILocalNotificationについて理解する
前回のゴール地点であるブランチadd-deadlineからスタートします
ブランチ local-notificationにチェックアウトすると解答を見ることができます
TODOの締切日時になったら端末から通知が表示されるようにします。
通知にはサーバーから通知を行うPush通知と、端末内で完結するUILocalNotificatoinの2種類があります。この二つの通知について見た目上の違いはありませんが、通知を送る元がサーバーか、アプリがインストールされている端末か、の違いがあります。
Push通知はシミュレータで利用できず、iOS Developer Programに登録しないと利用できないのと、Push通知を送るバックエンドを別途用意しないといけませ。UILocalNotificationはある位置に入った、あるいは時刻をトリガーとして通知を行います。
今回のケースではUILocalNotificationが適しているので、UILocalNotificationを利用します。
UILocalNotificationについては教材である 5. UILocalNotification に詳しく書いていますが、以下のステップで実装を行います。
まず、通知を出すことについて許諾を得る必要があります。これはアプリ起動時などでも構いませんが、アプリ起動時にいきなり
このようなアラートが出た場合、反射的に "Don't allow" と押してしまうユーザーも多くいます。
ここで許諾を得られない場合、通知を受けるのが非常に難しくなります。
(ユーザー自身が、設定から修正しないといけない)
そのため、通知されることに納得のいくタイミングで表示することが望ましいです。
今回はTODOを追加したタイミングでそのことをユーザーに許諾を得ればよさそうです。
そこで、TODO新規追加のコールバック関数内で以下のような処理を行います。
という手順になります。
通知の許諾を得たことを知らせるコールバック関数は appDelegateにあります。
この関数からViewControllerに知らせる必要がありますが、今回はNSNotificationを利用してプロセス内に通知します。
TODOアプリを作ってみようシリーズの第2回目の演習課題です
前回 ( #1 )に引き続き、今度は新しくToDoを入力し追加できるようにします。
前回のアプリをさらに発展させて、ToDoを追加できる画面を追加します。
ToDo一覧画面上部にナビゲーションバーを配置し、ToDo追加画面へ遷移するボタンを配置します。
追加画面は上部にキャンセルと完了ボタンがあり、それぞれタップすると�以下のように遷移します。
追加画面にはToDo用のUITextViewがありToDoを追加できるようにします。
ToDoは複数行入力できるようにしてください。
レギュレーション通りの画面を作るためには右上にボタンを設置する必要があります。iOSアプリでよく目にする、画面上部のバーは”UINavigationBar”です。
UINavigationControllerを用いるとこのUINavigationBarを利用することができます。
そのため、UINavigationControllerでメインのViewControllerを管理するようにstoryboardでViewControllerの構成を変更します。
また右上に+ボタンを追加します。これは”Bar Button Item” を選択してドロップし、設定を変更します。
最後にボタンタップ時のハンドラを追加して完了です。
先ほどの+ボタンをタップしたら新しく作成したViewControllerを表示させます。
UIViewControllerのサブクラスとして、AddTodoControllerのクラスを新規ファイルから作成します。
次にstoryboard上にViewControllerのオブジェクトを新たに配置し, クラスを”AddTodoController”, Storyboard IDを同じく”AddTodoController”とします。
また、先ほどのボタンタップ時のハンドラでこのAddTodoControllerを生成して表示します。
※この時、キャンセルボタンと完了ボタンを置くためにUINavigationControllerで表示を行います。
レギュレーションを反映するために、各パーツを配置し、Autolayoutを設定します。
テキスト入力エリアに使うクラスは UITextView です。複数行のテキスト入力や表示の際によく使われるクラスです。
このViewを配置して上下左右をpinで止めるAutolayoutを設定します。
キャンセルボタン、doneボタンはstoryboardからではなくコード上から追加します。
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
target:self
action:@selector(cancelButtonTapped:)];
このようにすると、NavigationBarの左上にキャンセルボタンが出現し、タップされるとcancelButtonTapped
が呼ばれます。
doneボタンも同様にします。
ここまでの状態でも入力はできるのですが、長い文章を入力するとキーボードの下に潜り込んでしまい、テキストを見ることができなくなってしまいます。
この不具合は次のコミットで直します。
�キーボードの下にTextViewが潜り込まないようにするためには”キーボードが表示されたタイミングでTextViewのサイズを変える”ことで対応します。
キーボードが出現した時には、UIKeyboardWillShowNotification のキー名でNSNotificationが通知されます。
NSNotification とは
NSNotificationとは、アプリ全体に行き渡る通知を行う仕組みです。
[NSNotificationCenter defaultCenter]
というシングルトンに対してどのオブジェクトがどのキーに対して通知を受け取るかのobserverを登録します。すると通知が発生したときに登録したメソッドが実行されます。詳しくはこちらをご覧ください → 7.4 NSNotification、NSNotificationCenter を用いた通知 · mixi-inc/iOSTraining Wiki
この通知を受け取れるようにobserverとして登録します。
(�注意: dealloc時にobserverを解除しないとクラッシュを引き起こします)
通知ごとの詳細は通知されるメソッドの引数であるNSNotificationインスタンスのuserInfoというNSDictionaryのプロパティの中に含まれています。
今回キーボードのサイズが必要なのですが、以下のようにして取得できます。
NSDictionary *userInfo = notification.userInfo;
CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
Auto layoutを用いてViewの配置を行っている時、Viewのサイズをプログラム上で調整したい場合、そのやり方はいくつかありますが、
簡単な方法の一つは”制約の値を変える”ということになります。
今回のケースだと、TextViewとViewControllerのViewとの間のマージンの制約の値を変更します。
この制約の値は初期値では0(つまりマージンなし)ですが、キーボードが出現したときにキーボードの高さの分だけ制約を大きくすれば良いとなります。
そのためには、以下のステップを経る必要があります。
となります。実行結果は以下のようになります。
わかりやすくするため、TextViewの色を緑にしてあります。
キャンセルボタン/Doneボタンが押された時に、AddTodoViewControllerを閉じ、追加があればToDoに追加を行います。
ToDoが追加された時、元のViewControllerはどのようにして新しく追加されたToDoを知るためにDelegateパターンを用います。
以下の処理を追加します。
TODOアプリを作ってみようシリーズの第一回目の演習課題のTODOです
UITableViewを利用して、TODOリストを表示します。TODOはコード内にハードコードしたものを用いて、追�加などは行いません。
UITableViewの使い方を理解する。以下のクラスの関係や基本的な使い方を学習することを目的としています。
また UITableViewCellをサブクラス化してカスタマイズも行います
以下のTODOをUITableViewを利用して表示します。
長いTODOも折り返して、全文が表示されるようにしてください。
(一部TODOらしくないTODOもあるのはそのサンプルです)
NSArray *todo = @[@"牛乳を買ってくる",
@"ビールを飲む",
@"家賃の振り込み",
@"洗剤を買い足す",
@"Macのアップデート",
@"ルンバの充電",
@"結婚式の招待状に返信する",
@"犬の散歩",
@"雨ニモマケズ 風ニモマケズ 雪ニモ夏ノ暑サニモマケヌ 丈夫ナカラダヲモチ 慾ハナク 決シテ瞋ラズ イツモシヅカニワラッテヰル 一日ニ玄米四合ト 味噌ト少シノ野菜ヲタベ アラユルコトヲ ジブンヲカンジョウニ入レズニ ヨクミキキシワカリ ソシテワスレズ",
@"ビールを飲む"
];
またTODOを表示するセルは以下のレギュレーションに従ってください.
UITableViewをメインのViewControllerの上に配置し、Autolayoutを設定します。
Autolayoutのpinの仕方は任意ですが、上下左右のマージンを0にするのがベターです。
さらにViewControllerのプロパティとして追加します。
tableView:numberOfRowsInSection:
はtodoの個数を返しますtableView:cellForRowAtIndexPath
は表示したいセルを返しますTodoTableViewCellという名前で新しいクラスを追加(xibも同時に作る)
カスタムセルの実装
※ iOS8以降の場合はAutolayoutを正しく設定することで自動的に高さを計算してくれる。iOS7以前もサポートする場合は、この高さの計算を行うメソッドを実装する必要があります。
ViewControllerがUITableViewDelegateに準拠するようにして、tableview.delegate = selfにする
@interface
// 中略
@property (strong, nonatomic) TodoTableViewCell *offscreenCell;
// 中略
@end
// viewDidLoadなどの中で
self.offscreenCell = [nib instantiateWithOwner:nil options:nil][0];
tableViewがレイアウトされたタイミング(viewDidLayoutSubviews
)でセルの横幅が決まる。
ここでoffscreenCellのサイズを決定する。(xibのサイズのままだと画面サイズが変わったときに対応できない)
この時ラベルに preferredMaxLayoutWidth
を設定する。これをしないと複数行の表示が正しく行われない
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
で各セルの高さを計算して返す
手順としては
start
※ Main.storyboard の initial view controller を指定し忘れたので、指定してください 😓
TODOアプリを作ってみようシリーズの発展課題その2 です。
TODO一覧の並び順は特に定めてなく、作成順で並んでいます。締め切り順に並べたり、タイトルのあいうえお順に並べた方が良い利用シーンなどもあります。
そこでTODO一覧を
で並び替えれるようにします。
UIの仕様については特に指定はありませんが、下図のようにUISegmentedControlを使うとよくiOSで見るレイアウトになると思います。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.