やりたいこと
- ニッチな自作iOSアプリ(タブバーコントローラーで複数のビューを持つ)にプッシュ通知を実装したい
- 同機能のAndroidアプリも作ってるので、プッシュ通知のフレームワークとしてFirebase FCMを使いたい
- プッシュ通知をバックグラウンド受信した場合、通知のタップでアプリ内の特定のビューを表示させたい
- プッシュ通知をフォアグラウンドで受信した場合、アラートを表示し、ユーザーによる「詳細を見る」のタップでアプリ内の特定のビューに遷移させたい(キャンセルなら何もしない)
- プッシュ通知をフォアグラウンドかつ非アクティヴな状態(コントロールセンター表示中など)で受信した場合、コントロールセンターが閉じられてアクティヴ化した直後にアラートを表示させたい
- 日本どローカルなアプリなので、各種表示の国際化はとりあえず考えない
画面遷移はこんな感じで 1
レギュレーション
- Xcode8.2
- iOS 8対応(それ以降の新APIはとりあえず捨て置き)
- Objective-C
- Rxってなんですか?
実装の要点
FCMの最低限の初期化
最低限これが必要です。iOS 8でMethod Swizzlingありならこれで大丈夫です(バッジを使っていませんが)。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// other initializing...
[FIRApp configure];
UIUserNotificationType types = UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
UIUserNotificationSettings *mySettings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
[application registerUserNotificationSettings:mySettings];
// other initializing...
return YES;
}
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {
[application registerForRemoteNotifications];
[[FIRMessaging messaging] subscribeToTopic:@"/topics/TOPICNAME"];
}
フォアグラウンド時に出すアラート
2か所から呼び出すことになるため、メソッドとして実装します。下の例では最後の方で「現在トップにあるビューコントローラーを取得する」黒魔術が唱えられています。
なお、引数でuserInfo
を渡せるようにします。プッシュ通知の内容を文字列としてべた書きしていますが、国際化するなら、私が前職で書いたこれが参考になるかもしれません。GCMの話ですがFCMでもほぼ同じです。
- (void)showPushNotificationAlertWithUserInfo:(NSDictionary *)userInfo {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:userInfo[@"aps"][@"alert"][@"title"]
message:userInfo[@"aps"][@"alert"][@"body"]
preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"詳細を見る", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
// 詳細を表示する
[self changeTab];
}]];
[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"キャンセル", nil)
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {}]];
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
[topController presentViewController:alertController animated:YES completion:nil];
}
4つのパターンを区別できるようにする
プッシュ通知がアプリにやってきたら、
- フォアグラウンドにいるようにユーザーに見えている場合はアラートを表示し、画面遷移の選択をしてもらう
- フォアグラウンドにいないようにユーザーに見えている場合はいきなり画面遷移する
とする、のが今回の目的です。
が、内部的には、アプリの状態は以下の4つのいずれかになっています。
- フォアグラウンドにある
- フォアグラウンドにあるが、コントロールセンターや通知センターがオーバーラップ表示されている
- プロセスがバックグラウンドに存在する
- プロセスが起動されていない
そして、この4つのパターンそれぞれを判別し、それぞれに応じてアラート表示なり画面遷移なりを行わなければなりません。
いろいろ調べてみましたが、プッシュ通知を受信した際に、以下の差異がある模様です。
# | didReceiveRemoteNotification |
その中でのapplication.applicationState
|
applicationWillEnterForeground |
didFinishLaunchingWithOptions |
---|---|---|---|---|
1. | 呼ばれる | UIApplicationStateActive |
呼ばれない | 呼ばれない |
2. | 呼ばれる | UIApplicationStateInactive |
呼ばれない | 呼ばれない |
3. | 呼ばれる | UIApplicationStateInactive |
呼ばれる | 呼ばれない |
4. | 呼ばれない? | - | 呼ばれる | 呼ばれる |
まず4.の場合、そもそもdidReceiveRemoteNotification
が呼び出されないことがあるようです。ただしこの場合は、アプリ起動のdidFinishLaunchingWithOptions
が呼ばれ、そこからuserInfoは取得できます。
それ以外の1.-3.はすべてdidReceiveRemoteNotification
で通知を受けられます。そしてその中でapplication.applicationState
の値を見ると、1.の場合はActiveになっており、これで区別できます。
問題は2.と3.です。これらについては、通知関連で取得できる状態はInactiveで区別ができません。
そこで、苦肉の策ですが、バックグラウンドでの受信かどうかを「直前のapplicationWillEnterForeground
呼び出しの有無」でフラグ判定するというきわめてあたまわるい方法で乗り切ることにしました。
結局、userInfo
も保持しておかねばならない
アラートを表示するのは1.および2.ですが、1.の場合はdidReceiveRemoteNotification
の中から直接表示すれば問題ありません。
一方、2.の場合は、その場でアラートを出すことは、非アクティヴなので不可能です。コントロールセンターが閉じられるとapplicationDidBecomeActive
が呼び出されますので、そのタイミングでアラートを表示することになります。
また、3.と4.では、didReceiveRemoteNotification
が呼び出されるかどうかという重大な違いがあります。そしていずれの場合でも、画面遷移をするのはアプリがフォアグラウンドに移行した後、すなわちapplicationDidBecomeActive
で処理したほうが無難かな、と考えました。
これらを勘案すると、結局、プッシュ通知の内容も、どこかに保持しておかねばならない、ということになります。
というわけで、変数を2つもかかえたコードが以下になります。
@interface AppDelegate ()
// バックグラウンドからの復帰時にYES
@property(nonatomic) BOOL fromBg;
// フォアグラウンド化した際に使うプッシュ通知内容
@property(nonatomic, strong) NSDictionary *initialUserInfo;
@end
@implementation AppDelegate
- (void)showPushNotificationAlertWithUserInfo:(NSDictionary *)userInfo {
// other initializing
NSDictionary *userInfo = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
if (userInfo) {
self.initialUserInfo = userInfo;
}
self.fromBg = YES;
return YES;
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
self.fromBg = YES;
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
// フォアグラウンド化時にやるべきことをやる
if (self.initialUserInfo) {
if (self.fromBg) {
// バックグラウンドからなのでいきなり詳細を表示する
[self changeTab];
self.fromBg = NO;
} else {
// コントロールセンターが閉じたのでアラートを表示する
[self showPushNotificationAlertWithUserInfo:initialUserInfo];
}
self.initialUserInfo = nil;
}
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
if (application.applicationState == UIApplicationStateActive) {
// アクティヴなのでアラートを表示する
[self showPushNotificationAlertWithUserInfo:userInfo];
} else {
// 非アクティヴなのでアクティヴ化した際に何かする
self.initialUserInfo = userInfo;
}
completionHandler(UIBackgroundFetchResultNoData);
}
タブバーコントローラー使用時にタブを切り替える
と言っても特に難しいことはありませんが、コードでタブバーに設定するビューコントローラーを追加している場合、UITabBarController
のselectedIndex
プロパティーは使えません。タブが6個以上の場合、右端のタブボタンは「その他」になります。またその「その他」を選んだ状態で、タブを入れ替える編集機能もあります。これらの場合にselectedIndex
で対応するのは無理筋で、代わりにselectedViewController
プロパティーを使うべきです。
そしてこのプロパティーに設定するビューコントローラーの値は…やっぱり保持かよ!!orz
@interface AppDelegate ()
// お知らせ表示用のナビゲーションコントローラー
@property(nonatomic, strong) UINavigationController *informationNavigationController;
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// other initializing...
// addTabViewControllerWithClassは別途実装
self.informationNavigationController = [self addTabViewControllerWithClass:[InformationViewController class] viewControllers:viewControllers customizedViewControllers:customizableViewControllers];
// other initializing...
return YES;
}
- (void)changeTab {
self.tabBarController.selectedViewController = self.informationNavigationController;
}
結論
- …早くこんな変数保持もレガシー言語もやめて、SwiftとRxで光のオーロラを身にまといたい…
脚注
-
画面に表示されている内容は架空のものです。東京メトロ訓練線は駅データベースには未来永劫登録されません。 ↩