UILocalNotificationのパターンとテスト

Presenter Notes

自己紹介

アイコン

Presenter Notes

アウトライン

通知 = ローカル通知(UILocalNotification)

  • 通知登録の前にキャンセル
  • 通知時間のチェック
  • 通知の繰り返しの方法
  • いつ通知を登録するか?
  • 通知登録とマルチスレッド
  • 通知の管理クラス
  • 通知のロジックテスト

Presenter Notes

通知登録の前にキャンセル

  • キャンセル方法
- (void)cancelLocalNotification:(UILocalNotification *)notification;
- (void)cancelAllLocalNotifications;

なぜ?

Presenter Notes

通知時間のチェック

  • 過去の時間をfireDateに設定すると登録した瞬間に通知が発生する
  • nilを設定した場合も同様
UILocalNotification *localNotification = [[UILocalNotification alloc] init];
localNotification.fireDate = PAST_DATE;
localNotification.alertBody = @"Fire!";
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];

対処法

  • 登録する前に fireDate が現在より後なのかをチェックする

Presenter Notes

fireDate のチェック

  • fireDateを設定する前に、NSDate をチェックする
    if (fireDate == nil || [fireDate timeIntervalSinceNow] <= 0) {
        return;// 現在より前なのでreturnする
    }

毎回チェック?

  • 毎回nilチェックは大変
  • 直接通知登録する代わりにラッパーを経由して登録する

Presenter Notes

登録用のhelperを作る

- (void)makeNotification:(NSDate *) fireDate alertBody:(NSString *) alertBody userInfo:(NSDictionary *) userInfo {
    if (fireDate == nil || [fireDate timeIntervalSinceNow] <= 0) {
        return;
    }
    [self schedule:fireDate alertBody:alertBody userInfo:userInfo];
}

- (void)schedule:(NSDate *) fireDate alertBody:(NSString *) alertBody userInfo:(NSDictionary *) userInfo {
    UILocalNotification *notification = [[UILocalNotification alloc] init];
    [notification setFireDate:fireDate];
    [notification setTimeZone:[NSTimeZone systemTimeZone]];
    [notification setAlertBody:alertBody];
    [notification setUserInfo:userInfo];
    [notification setSoundName:UILocalNotificationDefaultSoundName];
    [notification setAlertAction:@"Open"];
    [[UIApplication sharedApplication] scheduleLocalNotification:notification];
}

Presenter Notes

Example

    NSDate *tomorrow = [NSDate dateTomorrow];
    [self makeNotification:tomorrow alertBody:@"Fire!!" userInfo:nil];

メリット

  • 通知を登録する毎回条件分岐を入れる必要がなくて済む

デメリット

  • 明らかに未来の日付でも、NSDate をチェックするコストがある(多くても数ms程度…)

Presenter Notes

登録された通知の一覧取得

  • NSArray *scheduledLocalNotifications に登録済みの通知が入っている
  • NSDate をコンソールに表示するときはNSDateFormatterを使うと表示される時間がずれない
#if DEBUG
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setCalendar:[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]];
    [dateFormatter setDateFormat:@"yyyy/MM/dd HH:mm:ss"];
    for (UILocalNotification *notification in [[UIApplication sharedApplication] scheduledLocalNotifications]) {
        NSLog(@"Notifications : %@ '%@'",
            [dateFormatter stringFromDate:notification.fireDate],
            notification.alertBody
        );
    }
#endif

Presenter Notes

通知の繰り返し

  • repeatInterval プロパティに NSCalendarUnit を設定
  • 単純な繰り返し のみしかサポートされていない
  • 通知のダイアログから繰り返しを止める手段が用意されてない
    • 通知自体をキャンセルする必要がある

設定した fireData から

NSMinuteCalendarUnit : 1分毎
NSHourCalendarUnit   : 1時間毎
NSWeekCalendarUnit   : 1日毎
NSDayCalendarUnit    : 1週間毎

Presenter Notes

n分/n日ごとに通知を繰り返す

  • repeatInterval ではサポートされてない
  • プログラム的に、 n分ごとにずらした通知を設定していく

15 * 6 回 通知をつける例

    for (int i = 0; i <= 6; i++) {
        NSDate *fireDate = [basedFireDate dateByAddingMinutes:i * 15];
        [self makeNotification:fireDate alertBody:alertBody userInfo:nil];
    }

Presenter Notes

無限に繰り返す通知

  • そんなものはない
  • アプリを起動する度に、余分に通知を設定
    • 擬似的な無限の期間を繰り返す
    • アプリが余分につけた期間を超えると通知はでなくなる
  • 通知をキャンセルしてから付けるような仕組みじゃないとこのサイクルを保つのは難しい

Presenter Notes

ずっと鳴り続ける通知

  • 通知の音声ファイル(caf)指定する soundName
@property(nonatomic,copy) NSString *soundName;
  • 音声が鳴り続けている間、通知バーで表示され続ける(最大30秒)

NotificationBar

Presenter Notes

擬似的に鳴り続ける通知

  • 最大の30秒の音声を指定する(無音でもOK)
  • repeatInterval で 1分ごとに繰り返す
    UILocalNotification *notification = [[UILocalNotification alloc] init];
    [notification setRepeatInterval:NSMinuteCalendarUnit];
    [notification setSoundName:@"sound.caf"];// 30sec

結論

  • 電池をモリモリ食べる通知が完成!

Presenter Notes

いつ通知を登録するか?

  • データを保存するたび?
    • 保存処理に、通知登録をし直す処理を毎回追加する必要がある
    • 一番厳密なやり方
  • アプリを起動/フォアグランドするたび?
    • 登録する量が多いと他の処理と影響がでる
    • 別スレッドでの通知登録は後述
  • アプリを終了/バックグランドするたび?
    • 他の処理と混ざりにくい
    • 同期処理で登録してもあまり問題がない

おすすめ

  • バックグランド移行時に登録するのがシンプル

Presenter Notes

バックグランド移行時に登録

- (void)applicationDidEnterBackground:(UIApplication *)application {
    // schedule UILocalNotification
}

メリット

  • シンプル

デメリット

  • アプリ表示中にローカル通知を受け取って表示する処理がある等、条件によっては使えない

Presenter Notes

非同期の通知設定

  • dispatch_async など別スレッドで通知の設定は可能
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 非同期で通知を設定する処理
    dispatch_async(dispatch_get_main_queue(), ^{
        /* メインスレッドでのみ実行可能な処理 */
    });
});

メリット

  • UIスレッドが固まらずに済む

デメリット

  • マルチスレッドとCoreData等が絡むと面倒

Presenter Notes

通知の管理クラス

  • LocalNotificationManager という通知管理するクラスを考える
  • ローカル通知関係はこのクラスにまとめる
  • scheduleLocalNotifications を呼べば、一度全てキャンセルして登録し直す
  • バックグランドに移行時に、 scheduleLocalNotifications を呼ぶ
- (void)applicationDidEnterBackground:(UIApplication *) application {
    // バックグラウンドに移行際に通知を設定する
    LocalNotificationManager *manager = [[LocalNotificationManager alloc] init];
    [manager scheduleLocalNotifications];
}

Presenter Notes

LocalNotificationManager

@interface LocalNotificationManager : NSObject
// ローカル通知を登録する
- (void)scheduleLocalNotifications;
@end

@implementation LocalNotificationManager
#pragma mark - Scheduler
- (void)scheduleLocalNotifications {
    // 一度通知を全てキャンセルする
    [[UIApplication sharedApplication] cancelAllLocalNotifications];
    // 通知を設定していく...
    [self scheduleWeeklyWork];
}

// 例: weeklyWorkSchedule の時間を通知登録する
- (void)scheduleWeeklyWork {
    // ...
    // makeNotification: を呼び出して通知を登録する
}

#pragma mark - helper
- (void)makeNotification:(NSDate *) fireDate alertBody:(NSString *) alertBody userInfo:(NSDictionary *) userInfo {
    // 現在より前の通知は設定しない
    if (fireDate == nil || [fireDate timeIntervalSinceNow] <= 0) {
        return;
    }
    [self schedule:fireDate alertBody:alertBody userInfo:userInfo];
}

- (void)schedule:(NSDate *) fireDate alertBody:(NSString *) alertBody userInfo:(NSDictionary *) userInfo {
    // ローカル通知を作成する
    UILocalNotification *notification = [[UILocalNotification alloc] init];
    [notification setFireDate:fireDate];
    [notification setTimeZone:[NSTimeZone systemTimeZone]];
    [notification setAlertBody:alertBody];
    [notification setUserInfo:userInfo];
    [notification setSoundName:UILocalNotificationDefaultSoundName];
    [notification setAlertAction:@"Open"];
    [[UIApplication sharedApplication] scheduleLocalNotification:notification];
}
@end

Presenter Notes

LocalNotificationManager

Presenter Notes

UILocalNotificationのテスト

  • UILocalNotification / UIApplication
    • => ロジックテストから触れない - ref. iOS Unit Test
    • モックを作る必要がある
  • LocalNotificationManager のサブクラスを作る
    • UI--[LocalNotificationManager schedule:alertBody:userInfo:] のhelperに閉じ込めてある
    • helperメソッドをテスト用に上書きしたサブクラスを用意すればいい!
  • LocalNotificationManagerを継承した LocationNotificationManagerSpy を作る
    • schedule:alertBody:userInfo: を overwrite
    • 通知を登録する代わりに、登録された分の UILocalNotification をプロパティに貯める

Presenter Notes

ManagerSpyクラス

// テスト用にLocalNotificationManagerを継承したモッククラスを作る
@interface LocationNotificationManagerSpy : LocalNotificationManager
@property(nonatomic) NSMutableArray *schedules;
// helper
- (UILocalNotification *)notificationAtIndex:(NSUInteger) index;
// overwrite
- (void)schedule:(NSDate *) fireDate alertBody:(NSString *) alertBody userInfo:(NSDictionary *) userInfo;
@end

@implementation LocationNotificationManagerSpy
- (NSMutableArray *)schedules {
    if (_schedules == nil) {
        _schedules = [NSMutableArray array];
    }
    return _schedules;
}

- (UILocalNotification *)notificationAtIndex:(NSUInteger) index {
    if (index < [self.schedules count]) {
        return self.schedules[index];
    }
    return nil;
}

// 通知を登録するメソッドを乗っ取り、呼ばれたことを記録する(いわゆるspy)
- (void)schedule:(NSDate *) fireDate alertBody:(NSString *) alertBody userInfo:(NSDictionary *) userInfo {
    UILocalNotification *notification = [[UILocalNotification alloc] init];
    notification.fireDate = fireDate;
    notification.alertBody = alertBody;
    notification.userInfo = userInfo;
    [self.schedules addObject:notification];
}
@end

Presenter Notes

ManagerSpyクラスの中身

  • 通知登録 scheduleLocalNotification: は実際には呼ばれない
  • 通知登録の代わりに self.schedulesUILocalNotification が積まれていく

テストの流れ

  • 通常通り scheduleLocalNotifications を呼ぶ
  • self.schedules に入った通知情報が意図通りかをテストする
    • UILocalNotification のfireDate や userInfo が一致するかなど
  • - (UILocalNotification *)notificationAtIndex: はhelper

Presenter Notes

サンプル

参考

Presenter Notes