tvOSアプリのIn-App Purchaseで購入・リストア要求時にAppStore側からの応答が受け取れない問題
実際のところこの問題が発生するのは動作環境にも寄る気もするけど、tvOSアプリの実機デバッグで2台のAppleTV端末で同事象が発生したので、tvOSだと起こりやすいとかあるのかもしれない。
概要
In-App Purchaseで購入やリストアをする場合、ざっくり書くと以下のような流れでAppStoreとやりとりしながら処理する。
購入時
- プロダクトIDで初期化した
SKProductsRequest
のインスタンス(要求オブジェクト)を生成 - その要求オブジェクトの
start()
メソッドを実行し、指定したプロダクトIDが有効なプロダクトかAppStoreに問い合わせ SKProductsRequestDelegate
プロトコルを実装した処理でそのAppStoreからの応答を受け取る- 応答に問題なければトランザクションキュー(
SKPaymentQueue
)に支払い要求を追加し、AppStoreに送信 SKPaymentTransactionObserver
プロトコルを実装したトランザクションキューのオブザーバで応答を受け取る- 応答結果に応じた購入処理を行う
リストア時
SKReceiptRefreshRequest
のインスタンス(要求オブジェクト)を生成- その要求オブジェクトの
start()
メソッドを実行し、AppStoreにレシート更新要求を送信 SKRequestDelegate
プロトコルを実装した処理でそのAppStoreからの応答を受け取る- 応答に問題なければトランザクションキュー(
SKPaymentQueue
)でリストアプロセスを開始するよう、AppStoreに送信 SKPaymentTransactionObserver
プロトコルを実装したトランザクションキューのオブザーバで応答を受け取る- 応答結果に応じたリストア処理を行う
問題事象
手元の環境の場合、iOS実機デバッグでは特に問題は発生しなかったが、tvOS実機デバッグでは何故か上記「購入時」「リストア時」それぞれの「3.」に記載したAppStoreからの応答を受け取ることができない(SKProductsRequestDelegate
/ SKRequestDelegate
プロトコルを実装した処理が呼ばれない)という事象が発生。
原因調査
上記「購入時」の「1.」〜「3.」辺りの説明として、In-App Purchase プログラミングガイドのP15に以下の記載がある。
App Storeに問い合わせるには、プロダクト要求オブジェクトを使用します。最初に、SKProductsRequestのインスタンスを作成し、プロダクトIDのリストで初期化します。要求オブジェクトへの強い参照を保持してください。そうしないと、要求が完了しないうちに、システムが割り当て解除してしまうおそれがあります
併せてサンプルコードもガイドに記載されているが、SKProductsRequest
は必要がなくなるまで強参照を保持しておかないと、AppStoreへの要求が完了する前に要求オブジェクトの方が先に解放される可能性があるとのこと。
SKReceiptRefreshRequest
の方は特にガイドには同様の記載はなかったが、SKProductsRequest
もSKReceiptRefreshRequest
も共にSKRequest
のサブクラスのため、同じような事象が起こっているのではないかと推測。
対応
というわけで、ガイドに記載されているサンプルコードの通り、SKProductsRequest
、SKReceiptRefreshRequest
それぞれの要求オブジェクトをそのクラスのインスタンス変数に保持することで、ひとまず事象としては解決。
import StoreKit class YourPurchaseClass { fileprivate var productRequest: SKProductsRequest? fileprivate var refreshRequest: SKReceiptRefreshRequest? ... func startYourPurchaseProcess() { let productRequest = SKProductsRequest(productIdentifiers: ["PRODUCT_ID"]) self.productRequest = productRequest // 要求完了前に開放されないよう、インスタンス変数に保持 productRequest.delegate = self productRequest.start() } func startYourRestoreProcess() { let refreshRequest = SKReceiptRefreshRequest(receiptProperties: nil) self.refreshRequest = refreshRequest // 要求完了前に開放されないよう、インスタンス変数に保持 refreshRequest.delegate = self refreshRequest.start() } ... } ... // SKProductsRequest の要求完了後に呼ばれる extension YourPurchaseClass: SKProductsRequestDelegate { func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { // プロダクトID要求完了後処理 ... } } // SKProductsRequestDelegate の親クラス // SKProductsRequest と SKReceiptRefreshRequest 両方から呼ばれる可能性がある extension YourPurchaseClass: SKRequestDelegate { func request(_ request: SKRequest, didFailWithError error: Error) { if request is SKProductsRequest { // プロダクトID要求エラー時処理 ... self.productRequest = nil } else if request is SKReceiptRefreshRequest { // リストア要求エラー時処理 ... self.refreshRequest = nil } } func requestDidFinish(_ request: SKRequest) { if request is SKProductsRequest { // プロダクトID要求終了時処理 ... self.productRequest = nil } else if request is SKReceiptRefreshRequest { // リストア要求終了時処理 ... self.refreshRequest = nil } } }
SKRequestDelegate
の実装で、request(_:didFailWithError:)
が呼び出されるとrequestDidFinish(_:)
は呼ばれないとドキュメントに記載があるので、両方に要求オブジェクトの解放処理を入れている。
おわりに
ガイドの更新履歴を見るとこの記述は2015/10/21に追記されているようなので、それ以前に書かれたWebの記事とか本とかを参考に課金関連の処理を実装していると、この辺に触れられていなくてハマる。