Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

TODOの締切を入力できるようにする #5

Open
ginrou opened this issue May 14, 2015 · 3 comments
Open

TODOの締切を入力できるようにする #5

ginrou opened this issue May 14, 2015 · 3 comments

Comments

@ginrou
Copy link
Contributor

ginrou commented May 14, 2015

TODOアプリを作ってみようシリーズの第4回目の演習課題その1です。

内容

#4 に引き続き、この回ではTODOの締切日時を入力できるようにし、一覧でも表示できるようにします。

todo5

目的

  • UIDatePickerについて理解する
  • 実務でよくある、既存のレイアウトの修正を行う

教材

スタート地点

前回のゴール地点であるブランチ write-to-userdefauls からスタートします。

ゴール地点

ブランチ add-deadline にチェックアウトすると完成した様子を見ることができます。

アプリの仕様

このissueで取り組む改善では以下の仕様を追加します

  • TODOを追加するときに、TODO本文に加えて、TODOの締切日時を設定することができる
  • TODO一覧に締切日時が表示されている

画面のレギュレーションは以下のようになります。

TODO追加画面
  • TODO本文を入力するUITextViewに加えて、日付を入力するUIDatePickerを加えます
  • 本文欄、日付欄にそれぞれラベルを追加します
  • マージンは下記画像に従ってください

todo-add-deadline-01

TODO一覧画面のセル
  • TODOのタイトルに加えて締切日時をUILabelで表示します。
  • 各マージンは8pxとしています
  • (注意) UILabelを領域をわかりやすくするために背景をグレーにしています

todo-add-deadline-02

実装の方針

今回の実装は、デザイン面での修正はもちろんですが、データ構造にも変化があります。
今までのTODOはTODO本文のみを管理していましたが、今回は本文に加えて締切日時も加わります。
データ構造の変化がある場合は様々な箇所を変更していく必要があります。その点を留意して、デザイン面での修正と、データ構造など内部的な修正に分けて修正していきます。

まず、TODO本文と締切日時の両方を扱う方法について検討しましょう。TODOを扱う箇所は現在のところ

  • 新規作成画面での入力やdelegateでの引き渡し
  • 一覧に表示する
  • NSUserDefaultsへの保存

があります。それに対して、TODOのデータ構造には以下のようなサンプルが挙げられます。

  • (a) : todoの本文、日時をそれぞれ独立して持つ
    • ViewControllerのプロパティ に todoBodyListtodoDateListがあるようなイメージです
  • (b) : todoをNSDictionaryで保持する
    • NSDictionary *todo = @{@"body": 本文, @"date": 締切}; のようなイメージ
  • (c) : todoを新規クラスで持つ
    • このようなクラスのイメージ
@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のデータ構造は

  • 旧バージョン
    • todo本文のNSStringをNSArrayで保存
  • 新バージョン
    • todo本文と締切をNSDictionaryで一つのインスタンスとしてNSArrayとして保存

となるので、旧バージョンのデータ構造のまま新バージョンで起動すると以下のようにクラッシュが発生します。

// @[ @"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;
}

実装の解説

AddTodoViewControllerにDatePickerを追加し、レイアウトを調整 d4f750e

まずは新規追加画面で締切日時を追加するためのパーツを追加してレイアウトを調整します。
日付の入力にはUIDatePickerを利用します。 UIDatePickerは時間を指定するためのコンポーネントでUIKitに含まれています。UIDatePickerは時間の選択だけですが親クラスのUIPickerViewを利用すると任意のデータを選択することができます。

このUIDatePickerをstoryboard上に追加し、Autolayoutの調整を行います。このUIDatePickerの高さは固定で162ptとなっています。

配置が完了したらこのUIDatePickerをAddTodoViewControllerのプロパティとして追加します。
キーボードが出てきた時にPickerが隠れないようにすることを忘れないでください。

ToDoのエンティティをNSStringからNSDictionaryに変更 2b2c580

ToDoをNSStringで扱っていた箇所をNSDictionaryで扱うように変更していきます。
以下の該当箇所を直します

  • ToDo追加のコールバック関数
    • 呼ぶ側 (AddTodoViewControllerの- doneButtonTapped:)
    • 呼ばれる側 (ViewControllerの - addTodoViewController:addTodoCompleted)
  • ViewControllerのプロパティ
    • @property (strong, nonatomic) NSMutableArray *todo; は変えなくてもよい
    • todo[index] でアクセスするときに扱う型をNSDictionaryに変える
    • - tableView:heightForRowAtIndexPath:
    • - tableView:cellForRowAtIndexPath:
  • TodoTableViewCellは次のコミットで取り扱います

またここで、先ほど追加したUIDatePickerから締切日時を取り出しますが、これは datePicker.date で取り出すことができます。
戻り値はNSDateで、Objective-Cでよく使われる時刻のデータ型です。

TODOの締切を表示するようにした b39c70d

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" のような文字列になります
@mt-hodaka
Copy link

JSの即時関数パターンの感覚で書ける方法は無いかなと調べていてBlocksというのを知ったんですが、こういう使い方で正しいでしょうか。(メモリ管理上問題があるかな?とか、Obj-C的にどうなの?、というのをお聞きしたいです。)

// TODOの締切を表示するようにした
NSDateFormatter *dateFormatter = ^(){
    NSDateFormatter *df = [NSDateFormatter new];
    df.dateFormat = @"yy/MM/dd hh:mm";
    return df;
}();

このissueに関係ない質問ですみません。

@ginrou
Copy link
Contributor Author

ginrou commented Jun 1, 2015

@mt-hodaka さん
コメント頂いたBlocksの方法でもインスタンス初期化など可能です。
身近なコードではあまりこのような使い方は見かけないのですが、Block自体は非常に便利でよく使います。
また、Blocksを使う場合、循環参照するパターンがあるので注意が必要です。よくあるのがあるインスタンスがプロパティとしてBlocksを保持していて、そのインスタンスを保持し続けるパターンです。
そのような事故を防ぐために、Blocksで参照する変数は __weak をつけるなどの工夫があるのですが、詳しくは以下の資料をご覧ください。
https://github.com/mixi-inc/iOSTraining/wiki/8.1-Blocks

@mt-hodaka
Copy link

@ginrou さん
iOSTrainingにあったんですね!確認してませんでした。見てみますありがとうございますー。

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants