Swift3で消費型アプリ内課金(In-App-Purchase)を実装してみた

[自作アプリ](https://itunes.apple.com/jp/app/id1198443256)を作成するにあたり、アプリ内課金処理を実装したため、
備忘録を兼ねて公開したいと思います。

こちらのページを参考にさせて頂きました。
http://glassonion.hatenablog.com/entry/20111201/1322697417

なお、ここで記載しているコードは私の環境下で検証したものです。
課金に関わる部分なので、組み込む際にはご自身で良く検証してください。


# はじめに
Appleのアプリ内課金として販売できるアイテムは5種類あるのですが、
通常のアプリで使用するのは主に以下のアイテムです。

- Consumable(消費型)
  - 魚の釣り餌のような、***一回購入したら使い切り***のアイテム
- Non-Consumable(非消費型)
  - レースゲームの追加コースのような、***一回購入すれば恒久的に使える***アイテム

上記のうち、Consumable(消費型)の実装について記載していきます。

# 前提
- iTunesConnectに課金アイテムが登録されていること。
- SandBoxテスト用のアカウントを登録していること。
- 対象のXcodeProjectとClassにStoreKitがインポートされていること。

# 課金アイテムの情報取得
まずはじめに、iTunesConnectに登録してあるアイテムの情報を取得する必要があります。

```swift
let productRequest = 
SKProductsRequest.init(productIdentifiers: ["TestProduct1", "TestProduct2"])
productRequest.delegate = self
productRequest.start()
```

これでアイテム情報の取得が開始できます。
SKProductsRequestをinitする際に渡している値は、iTunesConnectに登録した
課金アイテムの"製品ID"を指定します。
※参照名ではないので注意

通信環境やAppleのサーバーの状態によっては情報取得に時間がかかるので、
実際の課金タイミングではなく、事前にやっておくほうが良いと思います。

# 情報取得後
```swift
func productsRequest(_ request: SKProductsRequest, 
            didReceive response: SKProductsResponse)
```

課金アイテムの情報取得が完了したら自動的にこのメソッドが呼ばれます。
このメソッドのresponseに取得した情報が入っているため、
無効なアイテムのチェックを行ってから実際の購入処理を開始します。
必要ならここで課金アイテムの現地価格なども取得できます。
(私はここまでをアプリ起動後に最初の画面でやってしまっています)


### 無効なアイテムチェック
```swift
if response.invalidProductIdentifiers.count > 0 {
    print("Error")
    return
}
```
### 価格取得
```swift
for product in response {
    // 現地のLocale
    let priceLocale = product.priceLocale
    // 現地通貨での価格
    let price = product.price
}
```

# 購入処理
```swift
func startPurchaseTransaction(identifier: String) {
    SKPaymentQueue.default().add(self)
    // response内のproductsから購入するproductsを取得
    for product in response.products {
        if product.productIdentifier == identifier {
            let payment = SKPayment(product: product)
            SKPaymentQueue.default().add(payment)
        }
    }
}
```

こちらは独自メソッドを作成しました。
自分自身をSKPaymentQueueのObserverにaddし、
引数として渡されたIdentifierに合致するproductをSKPaymentにaddします。
例えば、購入ボタン押下などの際に、押下されたボタンに対するidentifierを引数にして呼び出すような使い方になります。

# 購入処理中
購入処理中は購入の状態が変化し、その度に随時

```swift
paymentQueue(_ queue: SKPaymentQueue,
    updatedTransactions transactions: [SKPaymentTransaction])
```
このメソッドが呼ばれます。

購入状態は以下のようにしてハンドリングして、それぞれの状態に応じて適切な処理を行います。

```swift
func paymentQueue(_ queue: SKPaymentQueue,
    updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions {
        switch transaction.transactionState {
        case .purchasing:
            // 購入処理中
            // ここでインジケータを出すなどを行う
        case .purchased:
            // 購入完了
            // ここでアイテム付与など、アプリとしての購入完了処理を行う
            queue.finishTransaction(transaction)
        case .failed:
            // 購入失敗
            // ユーザーのキャンセル時にもここに来る
            queue.finishTransaction(transaction)

            if error.code == SKError.unknown.rawValue {
                // 支払い情報が無いなどで途中で止まってしまった処理を再開する
                SKPaymentQueue.default().restoreCompletedTransactions()
            } else if error.code == SKError.paymentCancelled.rawValue {
                // ユーザーによるキャンセル処理を行う
            } else {
                // 購入エラー処理を行う
            }
        case .deferred:
            // 承認待ち
            // どういうときにここに来るか不明
        default:
            // 念のため中途半端になったトランザクションをfinishする
            queue.finishTransaction(transaction)
        }
    }
}
```

# 購入終了処理
購入時の全てのtransactionが終了すると

```swift
paymentQueue(_ queue: SKPaymentQueue,
    removedTransactions transactions: [SKPaymentTransaction])
```
こちらのメソッドが呼ばれます。

サーバーサイドなどのデータと連動しているような課金アイテム
(ゲーム内でガチャが回せる　〜石とか)
の場合は、アイテムが正当なものかどうかを判断するために
レシートによるチェックなどをするらしいのですが、今回はそこまでは行っていません。


さて、ここまで来ると購入処理は完了ですので、SKPaymentQueueからObserverをremoveします。

```swift
SKPaymentQueue.default().remove(self)
```


# 最後に
課金処理って難しいし、ちょっと間違うとユーザーからのクレームに繋がるためビビってましたが、
Appleのframeworkを使うとCallBackがしっかり用意してあって思ったより大変じゃなかった印象。
レシート処理とかをちゃんとやるともう少し大変なのかな…

iOS課金処理を初めて実装したため、間違いや改善点などありましたらご指摘頂けるとありがたいです。