[Cocoa][Swift]StoreKit 2

<p>Apple Developerサイトでは、StoreKit 2という説明があるがフレームワークはStoreKitのまま。触ってみた感じは、既存のStoreKitを簡単に扱えるようにしたSwiftで実装されたライブラリだ。レシート検証もやってくれるようで、アプリ内で行う場合は、自力でASN.1でエンコードされたPKCS#7コンテナを解析するコードの実装が必要だったので助かる。</p>

<p>StoreKit 2は購読型の商品関連で便利になっているようだが、今回は消費型の商品のみとなっている。申し訳ない。</p>

<p>初めにStoreKit 2を利用する際に戸惑ったのは、Swiftのasync/awaitを前提としてAPIになっているので、既存のコードから呼ぶ際には、async/awaitへの対応が必要となる。</p>

<p>と言っても、凝らなければ恐れることはない。</p>

<p>StoreKit 2のAPIを呼び出す関数は async 宣言をする。</p>

<pre><code>func requestProducts() async {
}
</code></pre>

<p>そして、その関数を利用する側はタスクから呼び出す。</p>

<pre><code>Task {
    await requestProducts()
}
</code></pre>

<p>結果を渡す場合は、スレッドを意識する。例えば、こんな感じ。</p>

<pre><code>DispatchQueue.main.async { [weak self] in
    guard let self = self else { return }
    self.result = result
}
</code></pre>

<p>基本的な処理は以前と同様で、ストアから商品情報を取得するコードは以下となる。</p>

<pre><code>func requestProducts() async {
    do {
        self.storeProducts = try await Product.products(for: ["jp.co.bitz.Example.Consumable01", "jp.co.bitz.Example.Consumable02"])
        for product in storeProducts {
            appendDebugMessage(text: product.id + " " + product.type.rawValue + " " + product.displayPrice)
        }
    } catch {
        appendDebugMessage(text: "Failed product request: \(error)")
    }
}
</code></pre>

<p>商品情報の取得を要求するコードは上記3行目のコードのみとなる。async/awaitで記述されているため、以前のようで結果をdelegateで受け取るための関数を実装するということが必要なくなる。</p>

<p>商品購入は以下となる。</p>

<pre><code>func purchase() async throws -> Transaction? {
    if self.storeProducts.isEmpty { return nil }
    do {
        let result = try await self.storeProducts[0].purchase()
        switch result {
        case .success(let verification):
            let transaction = try checkVerified(verification)
            await transaction.finish()
            appendDebugMessage(text: "購入完了")
            return transaction
        case .userCancelled, .pending:
            return nil
        default:
            return nil
        }
    } catch {
        appendDebugMessage(text: "Failed product purchase: \(error)")
        return nil
    }
}
</code></pre>

<p>購入結果を受け取ったら、アプリかサーバでレシートを検証することになると思うが、以下がそのコード。</p>

<pre><code>func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
    switch result {
    case .unverified:
        appendDebugMessage(text: "レシート検証に失敗")
        throw StoreError.failedVerification
    case .verified(let safe):
        appendDebugMessage(text: "レシート検証に成功")
        return safe
    }
}
</code></pre>

<p>StoreKit 2でレシート検証をやってくれるようで検証結果を受け取るだけだ。</p>

<p>Ask to Buyや、購入処理中にアプリがクラッシュするなどで中断した場合の対応はどうやるか？これも以下のような簡素になる。</p>

<pre><code>typealias Transaction = StoreKit.Transaction
 
class ViewController: UIViewController {
    var updateListenerTask: Task<Void, Error>? = nil
 
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        updateListenerTask = listenForTransactions()
    }
 
    override func viewWillDisappear(_ animated: Bool) {
        updateListenerTask?.cancel()
        
        super.viewWillDisappear(animated)
    }
 
    func listenForTransactions() -> Task<Void, Error> {
        return Task.detached {
            for await result in Transaction.updates {
                await self.appendDebugMessage(text: "処理待ちのトランザクションがあった")
                do {
                    let transaction = try await self.checkVerified(result)
                    await transaction.finish()
                    await self.appendDebugMessage(text: "処理待ちの購入完了")
                } catch {
                    await self.appendDebugMessage(text: "Transaction failed verification")
                }
            }
        }
    }
}
</code></pre>

ソースコード
GitHubからどうぞ。
<a href="https://github.com/murakami/InAppPurchase">InAppPurchase</a> - GitHub<br />

【関連情報】
<a href="https://cocoa-kanto.connpass.com/">Cocoa.swift</a>
<a href="https://www.facebook.com/groups/cocoa.kanto/">Cocoa勉強会 関東</a>
<a href="http://www.bitz.co.jp/weblog/">Cocoa練習帳</a>