StoreKitトランザクションとFast App Switching

久しぶりにStoreKitについての記事です。In-App Purchase Programming GuideStoreKit Framework ReferenceにはFast App Switchingやスリープに関する説明がありません。しかし、購入手続きの途中にアプリ切り替え(Fast App Switching)が発生すると、アプリケーションは正しくトランザクションの状態変化を把握できなくなってしまいます。そこで、この問題についての対策を考えます。
※2012/05/22追記 この部分の挙動が変更になったようです。再検証を行ったのでこちらの記事を参照してください。

注意

この記事は、Apple公式ドキュメント類では説明/推奨されていない実装方法についての考察です。挙動や実装は将来的に大きく変わる可能性があり、動作は一切保証できません。参考にする程度にとどめ、もし下記に書かれている内容を実装する場合は自己責任で行ってください。

概要

前述してきた通り、アプリ内課金を実装していく上で厄介なのは購入手続き(トランザクション)の途中で、アプリケーション/デバイスの状態が変化し正しくトランザクションの状態変化を追えなくなることです。アプリケーションが終了して中断した場合については、過去の記事で説明しましたが、今回はFast App Switchingでアプリケーションがバックグラウンドに移行した場合、スリープした場合について見ていきましょう。

具体的なケース/再現方法

購入トランザクションの途中でFast App Switchingが行われた場合にどうなるか、下記の手順で検証していきましょう。

  1. AppStoreにプロダクト情報を取得しにいく
  2. Paymentを作成してキューに積む
  3. 購入しますか?ダイアログでOKを押す(必要があればAppStoreへのサインインを完了する)
  4. ダイアログが消えたらホームボタンを2回連続で押して適当なアプリケーションに切り替える。(トランザクションのステートがSKPaymentTransactionStatePurchasedになる前に完了させること)

アプリケーションがバックグラウンドに移行するため、この状態でSKPaymentTransactionStatePurchasedに変わった通知がどのように受け取れるのか、受け取れない場合どうするべきなのか、を見ていきます。

通常のパス

なお、Fast App Switchingによるアプリ切り替えを行わずに正しく購入手続きが進む場合、下記のような流れでトランザクションが完了します。

15:59:28.097 purchasing. <- トランザクション開始
15:59:31.941 purchased. <- SKPaymentTransactionStatePurchased
15:59:31.961 Transaction removed. <- トランザクション完了

※purchasingはSKPaymentTransactionStatePurchasingが通知されたタイミング、purchasedはSKPaymentTransactionStatePurchasedが通知されたタイミング、Transaction removedはpaymentQueue:removedTransactions:が呼ばれたときに出力しています。
トランザクション作成から4秒程で手続きが完了しました。

実験

上記の手順で、SKPaymentTransactionStatePurchasedが完了する前にアプリケーションを切り替えてバックグラウンドにしてみると、

16:08:42.131 purchasing.

ログはこの状態で止まり、バックグラウンド状態ではObserverはSKPaymentTransactionStatePurchasedの通知を受け取れません。
しばらくしてから再びFast App Switchingでアプリをフォアグラウンドに戻しても、SKPaymentTransactionStatePurchasedの通知は届きません。
このキャッチできなかったトランザクションの扱いについては、トランザクションが完了する前にアプリケーションが終了してしまった場合と同じく、次回アプリケーションを起動し直したときにPaymentQueueがシンクされるタイミングで再びObserverに通知されます。アプリケーションが終了せずにフォアグラウンドに戻ってきた場合の挙動も、トランザクション完了前にアプリを終了/再起動し、Queueの同期が完了する迄の間と同じような挙動になります。

対策

SKPaymentQueue Class ReferenceのaddTransactionObserver:の説明には、下記の文があります。

If there are no observers attached to the queue, the payment queue does not synchronize its list of pending transactions with the Apple App Store, because there is no observer to respond to updated transactions.

この説明自体は、「オブザーバーが一つも登録されていなかったら、pending状態のトランザクションを同期しないよ」ということを言っているだけなのですが、これを利用すると逆にフォアグラウンドに戻った状態でトランザクションを再開できるようです。

[[NSNotificationCenter defaultCenter]
  addObserverForName:UIApplicationDidEnterBackgroundNotification
  object:nil queue:[NSOperationQueue mainQueue]
  usingBlock:^(NSNotification *note) {
    [[SKPaymentQueue defaultQueue]
      removeTransactionObserver:self.observer];
    NSLog(@"observer removed");
  }
];
[[NSNotificationCenter defaultCenter]
  addObserverForName:UIApplicationWillEnterForegroundNotification
  object:nil queue:[NSOperationQueue mainQueue]
  usingBlock:^(NSNotification *note) {
    [[SKPaymentQueue defaultQueue]
      addTransactionObserver:self.observer];
    NSLog(@"observer added");
  }
];

具体的にはアプリケーションがバックグラウンドに入るタイミング(applicationDidEnterBackground)でobserverをremoveしておき、フォアグラウンドに戻るタイミング(applicationWillEnterForeground)で再びaddし直してあげると、フォアグラウンドに戻ったタイミングでバックグラウンドに入ったタイミングで中断していたトランザクションに関する通知を再び受ける事ができました。(iOS 5.0.1 / iPhone 4Sにて確認)

16:57:27.455 purchasing. <- 購入しますか?ダイアログでOKを押す
// ここでアプリケーションをバックグラウンドに移行
16:57:29.294 observer removed
// バックグラウンドのまましばらく待機(30秒程度)
// 再びフォアグラウンドに戻す
16:58:13.669 observer added
16:58:19.214 purchased. <- 通知された
16:58:19.237 Transaction removed. <- トランザクション無事完了

結論

公式ドキュメントで指示されているやり方ではありませんが、僕が試している限りではFast App Switching、スリープの対策は、他の方法で実現するのは難しいように思います。加えて、SKPaymentQueueのremoveTransactionObserver:というAPIは他に使う場面が思い浮かばないですし、基本は「バックグラウンドに入ったらobserveをやめ、フォアグラウンドに入ったら再びobserverしなおす」という方針が正しいのではないかと考えています。
お金が絡む大事な部分なので、In-App Purchase Programming Guideで公式な説明/指南が欲しいものです。

Pocket

「StoreKitトランザクションとFast App Switching」への3件のフィードバック

NotificationCenterを使ったFast App Switching対応 | なんてこったいブログ へ返信する コメントをキャンセル

メールアドレスが公開されることはありません。 が付いている欄は必須項目です