App Switcher に表示される View を差し替える補助ライブラリ MMAppSwitcher

以前アプリの画面を開いているアプリケーションのプレビュー画面から隠すというものを書きました。先日たまたま見つけたライブラリがこのような処理を補助するものがあったので一応ご紹介。

MMAppSwitcher というライブラリです。使い方的には MMAppSwitcherDataSource プロトコルを実装して App Switcher に表示して欲しい View を返す感じです。

ライブラリの内部的には UIApplicationWillBeginSuspendAnimationNotification という非公開な Notification をきっかけに App Switcher 用の View を表示させているようです。非公開なものなので若干審査だったり将来のバージョンアップとか大丈夫かな?って気がしますね。

こんなことをしている理由として推測するには、applicationDidEnterBackground: のタイミングだと

  1. アプリ最前面
  2. ホームボタン1回押し
  3. ホーム画面が表示されたと同時にホームボタン2回押し

というような操作をされた場合には applicationDidEnterBackground: がまだ呼ばれていないっぽいので App Switcher 用の View を用意できないんですね。だからこのライブラリの作者は willEnterBackground 相当のタイミングを探った結果 UIApplicationWillBeginSuspendAnimationNotification にたどり着いたのだと思います。

僕がやったような "ほぼ期待する動作をする" アプローチをとるのか、MMAppSwitcher のように攻めるのかは自己責任で。

どうでもいいけど

この「ホームボタン2回押し」で表示される機能ないし画面の Apple 的正式名称ってなんなんでしょうね?個人的に AppSwitcher って気持ち悪い。

iOS アプリ開発に関わる人にぜひ読んで欲しい本[の宣伝]

僕は iOS アプリのコーディングをやっていて主にそっち方面のブログエントリを書いていますが今日は本の紹介というかタイトル通り宣伝です。

アプリ開発をやってる僕ですが結構 UI / UX は気になるタイプで仕事中もデザイナーにいちゃもんつける面倒くさいやつなんですが今回同僚が本を書きました。

iOS 7デザインスタンダード 最新のフラットデザインに対応-iPhoneに最適なUI・UXを徹底的に解説!

iOS 7デザインスタンダード 最新のフラットデザインに対応-iPhoneに最適なUI・UXを徹底的に解説!

この本は主にデザイナー向けに書かれた本です。特に今まで UI / UX デザインを手がけた経験があまりない方や別プラットフォームがメインだったデザイナー向けです。僕の思ったターゲット読者別のおすすめポイントを簡単に紹介します。

デザイナー向け

先に挙げたように iOS アプリの UI / UX をバリバリやっている人向けというよりはこれから学ぶ人に最適だと思います。別プラットフォームが主戦場だった人にはチャプター 02 の iOS 標準 UI の紹介とその使いどころの説明で基礎的な知識を学べます。チャプター 03 はこの本の特徴だと思いますが実際に筆者がアプリの企画/デザインをする際にどのようなことを行っているのかを垣間見ることができる実践的な話が詰め込まれています。業務として UI / UX デザインの経験が浅い方はもちろん経験豊富な方も他のデザイナーがどのような作業フローをやっているのか?どのような視点で考えているのか?という事を知る機会は限られていると思うので1度読んでみると視野が広がるかもしれません。

開発者向け

iOS 6 から iOS 7 への標準 UI の変更点なども記載されていて iOS 7 向けのアプリ開発になれていない開発者の人でもその差異を知るのに役立ちます。巻末には各種画像のサイズや UI パーツの標準的なサイズがまとまっているのでちょっとリファレンス的に読むのもいいかもしれません。デザイナーがどんなことを考えてどのような視点でアプリのデザインを行っているか?ということは開発だけを行っていると知る機会の少ないのでこの機会に知ってみるのもいいと思います。

企画側・開発依頼側向け

デザイナーでも開発者でもないアプリの企画側の方や主にアプリ開発を依頼する側(ex: 既存 Web サービスのアプリ化を考えているような方)にも最適だと思います。デザイナーがどのような視点でアプリを良い物にするために考えているかを知ることができます。モバイルアプリの特性も知らずに考えているとあれもこれも載せたくなることは往々にしてありますが、この本を読めば本当にモバイルアプリとして iOS アプリとして必要な要素とは何なのか?ということに気づけると思います。

最後に

iOS アプリ開発に関わる人全てに1度読んでもらいたい本です。とても読みやすくすいすい読み進められる本なので是非。

iOS 7デザインスタンダード 最新のフラットデザインに対応-iPhoneに最適なUI・UXを徹底的に解説!

iOS 7デザインスタンダード 最新のフラットデザインに対応-iPhoneに最適なUI・UXを徹底的に解説!

おまけ: 別の同僚も iOS のデザインについて本を書いたのでそっちも読まないと。iOSフラットデザインの作法

dispatch_source の DISPATCH_SOURCE_TYPE_TIMER で timeout 処理を実装する

先日、x秒たったらある処理をキャンセルするといういわゆるタイムアウト処理を実装する必要があったときに dispatch_source を使ってハマったので備忘録。

当時ググっても繰り返し一定間隔で処理を動かすサンプルはすぐ見つかったのでそれをベースやっても期待する動きならなかった。結局は「エキスパート Objective-C プログラミング」にサンプルがのってて助かりましたという話。

Xcode のスニペット形式だとこんな感じ

    /// dispatch_source を生成。timer を回す queue を指定。
    dispatch_source_t timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, <#dispatch_queue#>);
    
    /// timeout の時間を "seconds" プレースホルダーに "15" のように入力。"leeway_seconds" にはズレの許容範囲を入力
    dispatch_source_set_timer(timerSource, dispatch_time(DISPATCH_TIME_NOW, <#seconds#>ull * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, <#leeway seconds#>ull * NSEC_PER_SEC);

    /// timer 発火時の挙動を実装
    dispatch_source_set_event_handler(timerSource, ^{
        
        <#code#>
        
        /// 後始末
        dispatch_source_cancel(timerSource);
        /* もしくは
#if !OS_OBJECT_USE_OBJC
        dispatch_release(timerSource);
#endif
        */
    });
    
    /// timer キャンセル時の挙動を実装
    dispatch_source_set_cancel_handler(timerSource, ^{
        <#something_clean_up#>
#if !OS_OBJECT_USE_OBJC
        dispatch_release(timerSource);
#endif
    });
    
    /// timer の動作開始
    dispatch_resume(timerSource);

素直に NSTimer を使えばいいじゃん?というのがあったんだけど、その処理全体がバックグランドディスパッチキュー上で実行されてたので Runloop を自分で回すのは嫌だったり、メインスレッドにタイマーをセットするとスレッドをまたぐ事になるのでシビアなタイミング(実行順序)を求められてたので dispatch_source を使いたかったというわけです。

ホントに「エキスパート Objective-C プログラミング」はすばらしい。

UICollectionView で UITableView のセクションヘッダー風の SupplementaryView を実装する

UICollectionView は昔なら UITableView を使って頑張って実装していようなグリッドレイアウトな UI を UITableView ライクな I/F で実装できる素敵なやつです。UITableView ライクな I/F とは言いましたが実は細かい挙動が UITableView とは違っています。UICollectionView で UITableView のセクションヘッダーのようなものを実装するには SupplementaryView を使います。でも普通に UICollectionViewFlowLayout を使っても SupplementaryView はスクロールすると Cell と同じようにそのまま通り過ぎてしまいます。UITableView のセクションヘッダーみたいに同一セクション内の場合に上端に居続けてくれません。UITableView のセクションヘッダーみたいなフローティングした(上端に張り付いたような)セクションヘッダーを実装したい。じゃあ、似たような挙動になるようにしようっていうのが今回の話です。

とりあえずググる

特にライブラリとしてまとまっているものが見つけられませんでした。(あとで調べたら vast-eng/uicollectionview-gridlayoutというのがありました。)しかし、stackoverflow で同じようなことをしたいという質問が見つかりそこから gist に上げられたコードを見つけました。

Sticky Headers at the top of a UICollectionView

やや難あり

さっきの gist ですが、確かに UITableView のセクションヘッダーみたいに同一セクション内の場合に上端に居続けてくれるのですが、全体的に位置が下がり気味。何かが違う。

どノーマルの UICollectionViewFlowLayout を使ったものがこちら

f:id:griffin-stewie:20131020120023p:plain

gist のコードをそのまま適用したのがこちら

f:id:griffin-stewie:20131020120021p:plain

修正

ずれる理由は UICollectionViewFlowLayoutsectionInset の値が考慮されていませんでした。修正後のコードはこちら。

UIEdgeInsets sectionInset = self.sectionInset;

origin.y = MIN(
                MAX(
                    contentOffset.y + cv.contentInset.top,
                    (CGRectGetMinY(firstObjectAttrs.frame) - topHeaderHeight - sectionInset.top)
                    ),
                (CGRectGetMaxY(lastObjectAttrs.frame) - bottomHeaderHeight + sectionInset.bottom)
                );

公開

修正したコードを UICollectionViewFlowLayout のサブクラスとして github にあげています。もし使えそうなら使って見てください。

CSNFloatingHeaderViewFlowLayout

CocoaPods でインストールできます。

pod 'CSNFloatingHeaderViewFlowLayout', '~> 0.0'

Thank you toblerpwn.

はてなインターンのサンプルコードを読んでの感想

はてなさんが

はてなインターンで利用したiOSアプリ等のサンプルコードを公開しました - Hatena Developer Blog

という素敵なサンプルコードを公開してくれたので、好き勝手に感想を書いてみたいと思う。本来は Web アプリ側も動かしたかったんだけど、ウマく環境が作れなかったのでアプリはコードを読んだだけで動かしてはないです。あと、細かい検証とかしてないので勘違いとかしていると思うので指摘してください。

参考になった真似したい

NSError と UIAlertView の使い方

NSError の内容ベースでエラーアラートを出すのは理にかなっていると思った。OS 側が返す NSError は期待する値が入っていない不親切な場合があるので、オリジナルの NSError から一度適切な値を持った NSError を再生成して返すクラスを介したらいいのかもしれない。

すっきりした UIViewController

iOS アプリを作り慣れていない人は特に直接ネットワーク処理を書いてしまったりモデルの管理コードを書いてしまいがちで UIViewController の肥大化するのがありがちなパターンだと思う。しかし、このサンプルはネットワーク処理、モデル管理等を綺麗に分けているのは見習うべきだと思った。

通信とモデルの管理の関係

普段僕は Web API を叩くクラスとそれを元にモデル生成するのを同じクラスに書いていたけど、このコードのようにネットワークはシンプルに Web API を適切に触ることだけにし、パースやモデルの生成、管理はモデル管理クラスに分けた方が綺麗だし、のちのメンテナンス性や拡張性が確保されていいと勉強になった。

isEqual:hash をちゃんと実装

結構面倒で実装してないときがあるんだけど、モデルクラスがちゃんと isEqual と hash メソッドをオーバーライドしているのが素晴らしい。特に hash メソッドを実装するのを忘れていると NSSet に突っ込んだ時の振る舞いが期待通りにならずに軽くハマるので注意。

KVO をウマく使っている

KVO を使って ViewController がモデルを見ているあたりは Mac アプリ実装経験者っぽくてなんか素敵。NSErrorlocalizedFailureReason とか使っている点も同様。KVO を使って、TableView の更新をかけているのは凄く勉強になる。mutableArrayValueForKey: は知らなかった。勉強になった。

NSIndexSet の使い方

indexSet をうまく使っているのは素敵だと思った。 mattt が WWDC でも言っていたので使う機会があったら自分も使いたいと思ってる。

気になったところ

UIAlertView+NSError

self.message = [[NSArray arrayWithObjects:[error localizedFailureReason], [error localizedRecoverySuggestion], nil] componentsJoinedByString:@"\n"];

一瞬、[error localizedFailureReason]nil だったら落ちるんじゃないかと思ったけど、よく考えると arrayWithObjects:nil 終端として動作するので落ちない。[error localizedFailureReason][error localizedRecoverySuggestion]nil なら self.message はカラのままで済むのでスマート。ただ実際にあるかどうかはわからないが、逆にいうと [error localizedRecoverySuggestion] が入っていて[error localizedFailureReason]nil を返す場合には、先に渡されている nil の評価がされてしまって仮に値が入っていても [error localizedRecoverySuggestion]self.messagenil となる。この挙動を忘れているとある日ハマる。もっとマジメに実装するなら NSMutableArraynil チェックをしながら足していく感じになるのかもしれない。

NSDictionary に入れる値の nil チェック

IBKMInternBookmarkAPIClient が引数の nil チェックもせずに NSDictionary に突っ込んでいるのは落ちる可能性があるので危険。使う側が意識すれば落ちないだろうけど、渡されたデータの妥当性チェックはデータをもらう側に責任があると思うのでチェックした方がいいと思う。

addObserver:~ したら remove~ する

KVO の監視開始、NSNotificationCenter への登録をしても、それらの解除を書いていないのは良くない。

Be sure to invoke removeObserver: or removeObserver:name:object: before notificationObserver or any object specified in addObserver:selector:name:object: is deallocated.

NSNotificationCenteraddObserver:selector:name:object: にも書いてある。

NSNotificationCenter の remove は複数回読んでも害はないけど、KVO の remove は必ず add を一対でないと行けない(add していない状態で remove すると落ちる)のでライフサイクル的に一対になるようなところに入れるしないといけないので注意が必要。

24時間表示を Off でもちゃんと機能するように NSDateFormatter を使う

NSDateFormatter に locale を "en-US" でセットしていないけど、24時間表示を Off にしている端末でも期待する動作をするのか不安。timeZone を指定していると大丈夫なんだろうか?未検証。

まとめ

気になる点はあるけど、これだけ綺麗にまとまって、なおかつそれなりに実戦経験がある人でもやってなさそうだったり知らなそうなことをサラッと見せてくれているので、このコードで Objective-C / Cocoa を勉強するインターン生は幸せもんだなぁと思った。僕も来年インターンで入れないかな?

アプリの画面を開いているアプリケーションのプレビュー画面から隠す

iOS 7 以前でも、アプリ実行中にホームボタンを押してまた、そのアプリに戻ってきた場合には OS が作成した画面のスクリーンショットが表示されていました。プライバシーが気になるようなアプリでは Home に戻る直前の画面をそのまま次回前面になったときに使われると困ることがあります。家計簿等のお金を扱うアプリだと独自に暗証番号でロックできるようになっていますが、私が使っている「マネー2」は前面に戻ってきた時にちらっと前の画面がうつってからロック画面に移行してて何だかなぁと思っていました。iOS 7 になってからはホームボタンを2回押して出てくる最近使ったアプリ一覧画面にプレビュー画面が付いて Exposé 風になりました。Exposé 風になったことでますますこの問題が出てきてしまって気になったのでどうすれば良いのか試してみました。

iOS App Programming Guide

iOS App Programming Guide には

Remove sensitive information from views before moving to the background. When an app transitions to the background, the system takes a snapshot of the app’s main window, which it then presents briefly when transitioning your app back to the foreground. Before returning from your applicationDidEnterBackground: method, you should hide or obscure passwords and other sensitive personal information that might be captured as part of the snapshot.

と書かれています。アプリ側でよしなに対処するべきなんですね。

普通に実装しただけの場合

普通に前面にアプリがいるとき

f:id:griffin-stewie:20131011010803p:plain

そのまま Home ボタン2回押し

f:id:griffin-stewie:20131011010752p:plain

当然ながら凜々しい猫が丸見えです。

対策を実装

今回試したのは割と手抜きです。試した内容は以下のような感じです。

  • AppDelegate に Window と同じサイズの UIView を用意し、適当な色をつけておく
  • 各種ライフサイクルのデリゲートメソッドでその View を 表示 / 非表示 させる

View の実装

- (UIView *)privacyView
{
    if (_privacyView == nil) {
        _privacyView = [[UIView alloc] initWithFrame:self.window.frame];
        _privacyView.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:0.95];
    }
    return _privacyView;
}

View の出し入れ

以下のようなメソッドを用意しておいて

- (void)showPrivacyView
{
    [self.window addSubview:self.privacyView];
}

- (void)removePrivacyView
{
    [self.privacyView removeFromSuperview];
}

Application のライフサイクルで出し入れを実装します。

- (void)applicationWillResignActive:(UIApplication *)application
{
    [self showBluredPrivacyWindow];
}

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    [self removeBluredPrivacyWindow];
}

applicationWillResignActive: はアプリが前面にあるときに Home ボタン2回押しの時も、Home に戻った時にも呼ばれます。applicationDidBecomeActive: はアプリが前面に戻ってきたときに呼ばれます。こちらもアプリが前面にあるときに Home ボタン2回押しした時も、Home に戻った時にも呼ばれます。

これらを実装すると以下のような感じになります。このスクリーンショットはアプリが前面にあるときに Home ボタン2回押しした時のものです。

f:id:griffin-stewie:20131011010741p:plain

折角なので iOS 7 っぽい磨りガラス風に

ということで iOS 7 風にしてみます。UIImage に対してブラーを掛けた画像を生成する部分は Apple のサンプルコードにある UIImage+ImageEffects を使ってみます。ただ、実際にはアプリの画面自体(View)を磨りガラス風画像にする必要があります。iOS 7 から - (UIView *)snapshotViewAfterScreenUpdates:(BOOL)afterUpdates がありますが、これは戻り値が UIView なので用途にあっていません。- (BOOL)drawViewHierarchyInRect:(CGRect)rect afterScreenUpdates:(BOOL)afterUpdates なら同様の処理を context に draw してくれるのでこちらを使って今の画面のスクリーンショットを取るようにします。

任意の View のスクリーンショットを撮って磨りガラス風の画像を生成するコードはこんな感じになります。

- (UIImage *)blurredSnapshotWithBlurType:(BlurEffectsType)type
{
    /// Original Code: iOS 7 blurring techniques — Damir Tursunović http://damir.me/posts/ios7-blurring-techniques
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, self.window.screen.scale);
    
    // There he is! The new API method
    [self drawViewHierarchyInRect:self.frame afterScreenUpdates:NO];
    
    // Get the snapshot
    UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();

    // Now apply the blur effect using Apple's UIImageEffect category
    UIImage *blurredSnapshotImage = nil;
    switch (type) {
        case BlurEffectsTypeLight:
            blurredSnapshotImage = [snapshotImage applyLightEffect];
            break;
        case BlurEffectsTypeExtraLight:
            blurredSnapshotImage = [snapshotImage applyExtraLightEffect];
            break;
        case BlurEffectsTypeDark:
            blurredSnapshotImage = [snapshotImage applyDarkEffect];
            break;
        default:
            break;
    }

    // Be nice and clean your mess up
    UIGraphicsEndImageContext();
    
    return blurredSnapshotImage;
}

ImageContext を作って、- (BOOL)drawViewHierarchyInRect:(CGRect)rect afterScreenUpdates:(BOOL)afterUpdates で画面を draw し、Context から画像化。その画像を磨りガラス風画像に再加工して書き出します。

実際にこれを使うのには先ほどのメソッドを少し変更します。

- (void)showPrivacyView
{
    self.privacyView.backgroundColor = [UIColor colorWithPatternImage:[self.window blurredSnapshotWithBlurType:BlurEffectsTypeLight]];
    [self.window addSubview:self.privacyView];
}

Window を磨りガラス風画像にして、UIColor にして view の backgroundColor に突っ込みます。綺麗に実装するのなら self.privacyView を UIImageView にするべきですね。この例では手抜き実装となっています。するとこんな感じになります。

f:id:griffin-stewie:20131011010920p:plain

シミュレータだと対策が無効の場合がある

ハマりどころとしては、シミュレータの場合、ホームに戻ってから最近使ったアプリ一覧画面をみても期待通りの動きをしません。実機で試せば問題ありませんでした。このせいで小一時間ハマりました。

まとめ

実際にやってみると簡単です。View を磨りガラス風の画像にするコードはカテゴリメソッド等で使い回しが効く処理なのでプライバシーに配慮するべき画面があるアプリでは実装してみてはいかがでしょうか?iOS 6 以下の場合は一部メソッドが使えないので少し違った対処が必要になるかと思いますが今回の方法をベースすればさほど難しくはないと思います。

サンプルコードをおいておきます。

griffin-stewie/PrivacyProtectSample

Remote Push からの Background Fetch を試してみた

前回は Background Fetch を試してみました。この方法ではアプリだけの実装でバックグラウンドで通信処理等を走らせる事ができますが、実行タイミングが OS 側の学習によるもので任意のタイミングでは発動させることができません。iOS 7 からは Background Fetch 以外にも Remote Push Notification をトリガーに Background Fetch を実行させる機能も追加されています。Remote Push Notification を送るためのサーバサイドの準備が必要ですが、任意のタイミングで実行できる(Rate Limit はあります)のは魅力的です。APNs 環境を作ったことがない僕が APNS 環境の構築から Remote Push Notification から Background Fetch を動かすまでのメモを残しておきます。

APNs 環境構築

今回、僕が使ったのは Heroku + Helios という環境を構築してみました。Heroku は個人開発者が動作検証に使う分には十分に無料の範囲でいろいろとためせて非常に助かります。Helios は AFNetworking の作者であり、Heroku 社員でもある Mattt が作った iOS デベロッパ向けの各種バックエンド機能を実装した Framework です。Heroku 社員でもあるだけに Heroku へのデプロイも簡単です。

まずは、Helios のインストール

gem install helios

次に Helios のアプリケーションを作成

helios new <任意のアプリケーション名>

これだけで必要なファイル群が作成され、git の管轄下になります。

今の Heroku は Ruby 2.0.0 で動いているので Gemfile に以下を追記。

ruby '2.0.0'

APNs に必要な pem ファイルを作成します。このあたりはあらゆるところに情報があるので割愛します。

作成した pem ファイルをディレクトリに追加。

pem ファイルを読み込み、APNs が使えるようにするための設定を config.ru ファイルに追記

service :push_notification, apn_certificate: './apple_push_notification.pem', apn_environment: 'development'

ここまでで下準備が大分整いました。あとは heroku 上にアプリケーションを作成します。 Heroku の基本的なセットアップは事前に終わっていることとします。

今回作成した Helios アプリの git リポジトリディレクトリ内で

heroku create

すると Heroku 上に環境を作ってくれます。このときに URL も出力されるので覚えておいた方があとあと便利です。ここまできたらあとは、

git push heroku master

すれば Heroku 上へのデプロイも完了です。

アプリへの組み込み

Bundle ID の修正

普段の開発ではプロビジョニングプロファイルをワイルドカード OK な設定にしてサンプルアプリを作っているときにはあまり意識していないかも知れませんが APNs では Bundle ID が合致している必要があります。まずは Bundle ID をそろえておきます。プロビジョニングプロファイルもAPNs が受信出来るものをセットします。

APNs を使うためのコード

何はなくとも APNs を使うためには Apple のサーバに登録しないといけません。application:didFinishLaunchingWithOptions: 内あたりに

[[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeNewsstandContentAvailability | UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)];

を書きます。個人的に試してみた感想では UIRemoteNotificationTypeNewsstandContentAvailability を宣言しておいた方が Remote Push Notification での Background Fetch が動きやすい気がしています。特にドキュメントに言及されていないように思えますので真偽のほどはわかりません。

Apple の APNs サーバーに対して登録を行ったあとは自身が用意する APNs サーバー側に取得したトークンを登録しておく必要があります。今回は Heroku 上にセットアップした Helios に対してこの作業を行います。今回は Helios を作った Mattt 作の ライブラリ Orbiter を使ってみます。Orbiter は AFNetworking ベースで作られた APNs 登録処理の補助ライブラリです。

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
    NSURL *serverURL = [NSURL URLWithString:@"http://foobarbuzz.herokuapp.com/push_notification"];
    Orbiter *orbiter = [[Orbiter alloc] initWithBaseURL:serverURL credential:nil];
    [orbiter registerDeviceToken:deviceToken withAlias:nil success:^(id responseObject) {
        NSLog(@"Registration Success: %@", responseObject);
    } failure:^(NSError *error) {
        NSLog(@"Registration Error: %@", error);
    }];
}

上記コードを書くだけでとりあえず動きます。個人的にハマったのは URL です。Orbiter のサンプルだと Heroku 上に作成したアプリの URL を書いていますが、これではダメでした。ググったりして調べてみると Helios 側のソースコードを除いてみたところ rack/push-notification に丸投げしてるようです。ということで http://foobarbuzz.herokuapp.com ではなく http://foobarbuzz.herokuapp.com/push_notification を指定します。Helios の github の README にも書いてありました。

これでアプリを起動して、orbiter の Registration Success まででれば OK。

意気揚々とビルドして

“aps-environment”エンタイトルメント文字列が見つかりません

とか言われたら正しいプロビジョニングプロファイルをセットしていない可能性があるので確認しましょう。僕は2回もやらかしました。

実際に Push を送る

Helios の README には curl を使った例があるのですが、Remote Push Notification による Background Fetch を実行するために必要な "content-available" を送っても Helios 側が受け取れません。ソースコードを読んでみるとそもそも "content-available" には対応してなさそうです。ということでまたまた Mattt 謹製のライブラリ Houston を使って Push を送ります。

houston のインストールは

gem install houston

もしくは他の関連コマンドもまとめてインストールできる nomad を入れても良いかもしれません。

gem install nomad-cli

インストールができたら普通の Push を送ってみます。

apn push DEVICE_TOKE -c PATH_TO_PEM_FILE -m "Test Push"

DEVICE_TOKE は APNs に登録した際に取得したトークンです。Helios 側にも登録したはずなので、http://foobarbuzz.herokuapp.com/admin/#push-notification のように自身の Heroku アプリの URL にアクセスすることでもわかります。 PATH_TO_PEM_FILE は Heroku にデプロイした pem ファイルへのパスです。 -m オプションは Push に乗せるメッセージです。

これで実機で受信できれば OK です。

Remote Push から Background Fetch

前回と同様に Xcode 5 の Capabilities を開いて、Remote notifications にチェックをつけておきます。

次にメソッドの実装。これは前回と似たようなメソッド - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler を実装します。

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    if(![userInfo[@"aps"][@"content-available"] intValue])
    {
        completionHandler(UIBackgroundFetchResultNoData);
        return;
    }
   
    StepCountTweeter *tweeter = [[StepCountTweeter alloc] init];
    [tweeter tweetStepCountWithUserName:nil
                         tweetTextBlock:^NSString *(NSInteger numberOfSteps, NSDate *fromDate, NSDate *toDate, NSError *error) {
                             NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
                             [dateFormatter setLocale:[NSLocale systemLocale]];
                             [dateFormatter setDateFormat:@"MM/dd HH:mm:ss"];
                             NSString *text = [NSString stringWithFormat:@"%@%@ の期間に iPhone 5s を持って %@ 歩 歩きました。 Tweet By RemoteNotification Fetch"
                                               , [dateFormatter stringFromDate:fromDate]
                                               , [dateFormatter stringFromDate:toDate]
                                               , [@(numberOfSteps) stringValue]];
                             
                             UILocalNotification *localNotif = [[UILocalNotification alloc] init];
                             localNotif.timeZone = [NSTimeZone defaultTimeZone];
                             localNotif.alertBody = text;
                             [[UIApplication sharedApplication] presentLocalNotificationNow:localNotif];
                             
                             [self updateBadgeCountWithCurrentDate:toDate];
                             
                             return text;
                         } completionBlock:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
                             if (error) {
                                 UILocalNotification *localNotif = [[UILocalNotification alloc] init];
                                 localNotif.timeZone = [NSTimeZone defaultTimeZone];
                                 localNotif.alertBody = [error description];
                                 [[UIApplication sharedApplication] presentLocalNotificationNow:localNotif];

                                 completionHandler(UIBackgroundFetchResultFailed);
                             } else {
                                 completionHandler(UIBackgroundFetchResultNewData);
                             }
                         }];
}

"content-available" をチェックしているのは Fetch 処理が必要ない場合に呼ばれたときに余計な通信をしないためにしています。本当はもう少しいろんなシチュエーションに応じて分岐処理が必要かも知れません。

Push から発動させるには apn コマンドに -n オプションをつけておきます。

apn push DEVICE_TOKE -c PATH_TO_PEM_FILE -n -m "Test Push"

Silent Push

次に Silent Push を送ってみます。Silent Push とは Payload にメッセージを含めず "content-available" のみを送るものです。この Silent Push の場合 Remote Push Notification による Background Fetch は発動しますが、通常の Push のように Push そのものは Notofication Center に表示されないことから Silent と呼ばれています。実装によっては、Silent Push を受け取り Background Fetch で動作が完了した際に Local Push を実行するということも出来ます。このような実装をした場合にはエンドユーザーに取っては Notification Center の通知が来たと同時にアプリを開けばその内容に応じたコンテンツがすでに取得済みということが出来ます。

Silent Push の送り方ですが、ここでも難点が。私の環境では

apn push DEVICE_TOKE -c PATH_TO_PEM_FILE -n

とするとエディタが開いてしまいます。

apn push DEVICE_TOKE -c PATH_TO_PEM_FILE -P '{"aps": {"content-available":1}}'

とするか、Houston のサイトにあるようにスクリプトとして実装してしまう方法をとりました。

私の環境では Silent Push を基点に Background Fetch を動かすのは Rate Limit に引っかかっているせいなのか多少間をあけないと実行できませんでした。

トラブルシューティング的ななにか

  • push を登録するときには UIRemoteNotificationTypeNewsstandContentAvailability を宣言しておいた方が Remote Push Notification での Background Fetch が動きやすい気がする
  • うまく Push が発動しない時には unregister してみる
  • いきなり込み入ったことをせずにシンプルにバッジの更新程度で試してみる

感想

期待通りに動かない時があったりしたので、実際にタイムリーに発動させるためにはもう少し研究が必要な気がしますが期待度の高い機能なので取り組んでみる価値は高いと思いました。