2014年8月12日火曜日

Apple Push Notification (APN) 使用時の delegate の挙動について、 iOS 7以降 / iOS 6以前の差をまとめた

iOS 7以降とiOS 6以前で、俗にいうリモートPush通知の受け取り方と受け取った際の挙動がまるで違っているので、最近リモートPush通知を実装した時につまづいた箇所をまとめてみました。

使用するdelegate methodの違い

iOS 7以降


  • いかなる種類のPush通知においてもapplication:didReceiveRemoteNotification:fetchCompletionHandler:を使用します。
  • application:didReceiveRemoteNotification:fetchCompletionHandler:とapplication:didReceiveRemoteNotification:が両方実装されている場合も、application:didReceiveRemoteNotification:fetchCompletionHandler:しか呼び出されません。application:didReceiveRemoteNotification:fetchCompletionHandler:のみ考えればOKです。
  • ただし、application:didReceiveRemoteNotification:が実装されており、application:didReceiveRemoteNotification:fetchCompletionHandler:が存在しない場合は、iOS 6以前同様の挙動になります。
  • 以下、長ったらしいのでapplication:didReceiveRemoteNotification:fetchCompletionHandler:を新メソッドと呼称します。

iOS 6以前


  • application:didReceiveRemoteNotification:を使用します。
  • 新メソッドが実装してあっても一切反応しません。
  • 以下、長ったらしいのでapplication:didReceiveRemoteNotification:を旧メソッドと呼称します。

挙動の違いまとめ

以下に表にしてまとめました。ここで、
iOS 7以降とは新メソッドを使用する場合、iOS 6以前とは旧メソッドを使用する場合のことを指します。


iOS 6以前iOS 7以降
初回起動application:didFinishLaunchingWithOptions:に
UIApplicationLaunchOptionsRemoteNotificationKeyが付いて呼び出される。
旧メソッドは呼び出されない。
application:didFinishLaunchingWithOptions:に
UIApplicationLaunchOptionsRemoteNotificationKeyが付いて呼び出される。
その後、UIApplicationStateActiveの状態で、新メソッドも呼び出される。
handlerについては、content-available指定が1の場合のみ付いてくる(未確認)。
起動中UIApplicationStateActiveの状態で、旧メソッドが呼び出される。UIApplicationStateActiveの状態で、新メソッドが呼び出される。
handlerについては、content-available指定が1の場合のみ付いてくる(未確認)。
未起動UIApplicationStateInactiveの状態で、旧メソッドが呼び出される。UIApplicationStateInactiveの状態で、新メソッドが呼び出される。
handlerについては、content-available指定が1の場合のみ付いてくる(未確認)。
データ取得不可能。UIApplicationStateBackgroundの状態で、新メソッドが呼び出される。
handlerは必ず付いてくる。
この通知がアラートや通知センターに表示された場合、ユーザーがそれらとインタラクションしたらさらにもう一回UIApplicationStateInactiveの状態で新メソッドが呼び出される。

バックグラウンドでの通知受け取りとデータ取得

iOS 7から、Push通知のペイロードのapsオブジェクトにcontent-availableというプロパティが新たに追加されました。content-availableプロパティの値を数字の1に設定すると、ユーザーのインタラクションを介さずにバックグラウンドでアプリケーションが起き上がることができ、サーバから最新のデータの取得を行ったりする事が可能です。詳しくは「Push通知 バックグラウンド iOS7 content-available」とかその辺の単語を適当に組み合わせてググってください。山ほど解説しているページがあります。

サイレント通知が届かないときの処方箋

さて、上記バックグラウンドでの通知受け取りとデータ取得について、余り解説が見当たらなかった点についてまとめました。

iOS 7以降のPush通知は、apsオブジェクトのalertプロパティの有無とcontent-availableプロパティの有無によって挙動が変わります。具体的には以下の表のようになります。

content-availableがあるcontent-availableがない
alertがある通常通知
同時にバックグラウンドデータ取得も可能
通常通知
alertがないサイレント通知
バックグラウンドデータ取得のみ可能
送信できない

content-availableがない場合はiOS 6以前のPush通知と全く同じなので説明を割愛します。次にcontent-availableとalert両方が指定されている場合ですが、これはiOS 6以前の通常通知と基本は同じで、異なる点は通知を受け取った瞬間にバックグラウンドでアプリが立ち上がってデータの取得ができるという点だけです。したがいましてちょっと上記に記載しましたが、通知を受け取った瞬間と、ユーザーがインタラクションした瞬間で2回新メソッドが呼び出されるので、必ず新メソッドの中でUIApplicationStateを見て処理を分岐するようにしてください。

問題になってくるのがcontent-availableだけが指定されている通知、サイレント通知についてです。ほかのサイトでもよく紹介されていますが、この通知を使ってバックグラウンドでアプリを起動させて最新のデータをダウンロードしたりすることができます。以下にサイレント通知のペイロードの例を示します。

上が通常のサイレント通知ですが、下のようにサイレント通知と同時にバッジの数を更新することもできます。

さてこのサイレント通知ですが、とにかく「届かない!」というトラブルを耳にします。実際私が試した際にもなかなか届かないで弱りました。そこで調査したところ、サイレント通知のペイロードを工夫することできちんと通知が到達するようになりました。以下に到達率を改善したサイレント通知のペイロードの例を示します。

ご覧のように、空のsound要素を追加することで到達率が劇的に向上します。また2つ目の例のように、priorityというプロパティを10に設定するとこれまた到達率が劇的に向上します。どうやらこのpriorityプロパティはalertプロパティかsoundプロパティが設定されている場合はデフォルト10に、設定されていないサイレント通知の場合はデフォルト5に設定されているようで、それが原因で到達しない事が多いということがわかりました。
参考記事: http://stackoverflow.com/questions/19239737/silent-push-notification-in-ios-7-does-not-work

それから、Xcode上からプロジェクトのCapabilitiesの設定をするとき、Background fetchとRemote notificationsの両方の指定がおそらく必須と思われます。中にはRemote notificationsだけ設定しておけば良いと解説しているところもありますが、どうやらRemote notificationsは通知を受け取る箇所までだけで、肝心のバックグラウンドでのデータの取得はBackground fetchがないと実行できないのではないかと思われます。

UINavigationController.interactivePopGestureRecognizer の挙動を魔改造して変えてみる

注意: 今回の記事は久しぶりの誰得記事です。こちらで紹介されている手法を用いるにはプライベートAPIを操作する必要があるため、おそらく審査に通りません。じこせきにんでお願いします。

iOS 7からUINavigationControllerにinteractivePopGestureRecognizerというプロパティが新たに追加されました。これは「ナビゲーションコントローラの左端から右にスワイプすると前の画面に戻る」機能を司るgesture recognizerで、同じくiOS 7から追加されたUIScreenEdgePanGestureRecognizer(画面端からのみ反応するPan Gesture Recognizer)とUIViewControllerInteractiveTransitioning(ViewController間の画面遷移時に、ユーザーのインタラクションを追加する。例えば画面半分までスワイプしたら50%画面遷移するとか)の仕組みで実装されています。

ここで、例えばこの「ナビゲーションコントローラの左端から右にスワイプすると前の画面に戻る」機能について、見た目はそのままがいいんだけど左端からしか反応しないのが気に入らないから画面の中央でも反応するようにしたいという要望があったとします。自前で実装してもよいのですが、やはりApple純正の動きが良いですよね。

そこで以下のページで紹介されている黒魔術を使ってみたいと思います。
http://blommegard.se/blog/2014/01/31/a-take-on-custom-transitions-with-uinavigationcontroller/
こちらのページにUINavigationController内部でどのようにinteractivePopGestureRecognizerが実装されているかという解説があります。

  • 先述の通り、UIScreenEdgePanGestureRecognizerのインスタンスが使用されている
  • targetは_UINavigationInteractiveTransition型のオブジェクト、調査の結果_cachedInteractionControllerという隠しプロパティがそれに相当することがわかった
  • actionは@selector(handleNavigationTransition:)

ここまでわかったので、あとはtarget/actionを自分で生成したUIGestureRecognizerにセットしてやれば、標準の動きをジャックすることができるというわけです。今回はUIPanGestureRecognizerを使用しました。



このコードを実行してみると確かに画面中央からでもいつものスワイプジェスチャで戻る挙動が可能になっています。やりましたね\(^o^)/

2014年6月25日水曜日

App Extension や Widgets に App Container を使わないで簡単にデータを渡す方法

iOS 8から利用できるApp Extensionはアプリ本体とは別のプロセスとして動作するため、そのままでは簡単にデータを渡すことができません。公式のドキュメントではApp Containerという仕組みを使う方法が推奨されていますが、この方法はiOS Dev Center上でApp IDの設定が必要になったりするなど面倒がつきまといます。そこでより簡単にApp Extensionにデータを渡す方法がないか調べてみました。

方法1: UIPasteboardを使う方法

一番おすすめなのがこちらです。UIPasteboardというのは要するにコピペ用のクリップボードみたいなものなのですが、こちらシステムが用意している共通のもの以外にも自分のTeam IDでコードサインされたアプリ間すべてで共用できるPasteboardを作成する機能があります。この機能を使用すると非常に簡単にデータをやりとりすることができます。面倒な設定も不要ですし、persistentプロパティを設定することでディスクへの永続化も面倒を見てくれます。

以下にサンプルコードを掲載します。本体アプリのUIApplicationDelegate内でクリップボードを作成してデータを突っ込み、App ExtensionのWidgetViewController内でその値を使用するサンプルです。

注意点として、UIPasteboardのnameはあなたの作成したアプリ全体(同じTeam IDでコードサインされたアプリ全体)で共有になります。ですのでWidgetですとかTestのような単純な名前ではなく、com.akisute.MyApp.Widgetのような一意になる名前をつけることをおすすめします。

UIPasteboardについての詳しい使い方については、以下の記事をご覧ください。

方法2: Keychainを使う方法(未確認)

未確認なので確定情報ではありませんが、Keychainには複数のアプリ間で値を共有する機能がありますので、そちらを使えばおそらくアプリ本体とApp Extensionの間で値を共有する事が可能になります。詳しくは以下の記事をご覧ください。