swiftUIでiOSアプリをリリースしたけどStoreKit周りの実装が大変だった話

# はじめに
swiftを勉強しはじめて約3ヶ月の筆者なのですが、先日はじめてApp Storeにアプリをリリースすることができました👏

今回はサブスクリプションのアプリケーションを作成したのですが、その際の実装が結構大変だったので本記事にまとめます。
興味あれば是非見ていってください！

# 開発したアプリ
今回はopen AI APIを用いてギャルやお兄さんなど、色々な属性のAIとチャットを楽しめるアプリをswift UIで開発しました！
興味があれば是非一度ご利用してみてください〜
※月額600円のサブスクリプション制です

https://apps.apple.com/jp/app/anychat-with-ai/id6468811282

# StoreKitでのサブスクリプションの実装
このアプリケーションではStoreKitというパッケージを利用して課金周りを実装したのですが、中々日本語の記事が見つからず、結局英語の記事を参考に実装したので、皆さんも実装する際の助けになれば。

そもそもStoreKitとは？という方はまず以下の公式ドキュメントをご参照ください
https://developer.apple.com/jp/storekit/

今回私がstoreKitで課金周りを実装するにあたり参考にした記事は以下のものです
https://www.revenuecat.com/blog/engineering/ios-in-app-subscription-tutorial-with-storekit-2-and-swift/?utm_source=google&utm_medium=cpc&utm_campaign=Alpha&utm_content=%22storekit%202%22&utm_ad=647866164209&utm_term=storekit%202%20tutorial&matchtype=e&device=c&GeoLoc=9157995&placement=&network=g&campaign_id=17824399608&adset_id=146872078238&ad_id=647866164209&gclid=Cj0KCQjwr82iBhCuARIsAO0EAZwlbFTzo0GHDHw8HcYfzkxlE33JH_cAK9Pe0Khc3ceW6GUm2hO2aSIaAuaQEALw_wcB

## 1. App Store Connectの登録

iOSアプリなどをApp Storeでリリースするためには、まずApp Store Connectというところに登録する必要があります。
その設定周りの話は色々な記事がすでに存在するので、それをご参照ください〜
https://zenn.dev/moutend/articles/feebf0120dce6e6426fa

## 2. サブスクリプションの作成
1の手順でアプリケーションをApp Store Connectで作成し終わっている前提です。
App Store Connectでアプリケーションのページに移動すると以下の画像のような画面になると思います
![スクリーンショット 2023-11-03 20.07.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1193085/057d974d-fc01-c84e-b25e-f337e176b14e.png)

ここで左のバーの中にある「機能」の欄の「サブスクリプション」を選択します。

ここでまず、サブスクリプション商品を登録する前に、その親となるサブスクリプショングループを登録します。
その後、作成したサブスクリプショングループをクリックすると、表示される画面の案内に従ってサブスクリプションを登録します。
サブスクリプションを登録したら、サブスクリプション期間、サブスクリプション価格を設定し、また、App Storeのローカリゼーションの欄から表示名と説明を登録します。

これで一通りのサブスクリプションの作成自体は完了です。
次にStore Kitを用いてローカルで色々と設定していきます

# 3. Store Kit構成ファイルの準備
アプリ内課金を実装するなら当然、ローカル環境でのテストはしたいと思います。
なのでまずは上で作成したサブスクリプションをローカルに紐づけていきます

## ファイルを作成する
以下にセットアップするためのファイルの作成手順を記載します
1. Xcodeの[ファイル] > [新規作成] > [ファイル]を選択
2. ファイルの種類の選択画面で「StoreKit Configuration File」を選択
3. 名前をつけて、「Sync this file with an app In App Store Connect」にチェックを入れて保存
これでApp Store Connectと同期され、先ほど作成したサブスクリプションがローカルでも登録されました。
※これはXcode14以降の話です。Xcode13以下の場合は、追加の手順が必要となる場合があります。

## Store Kit configuration fileを有効にする
上で作成したStore Kit configuration fileをローカルで確認するためにはSchemeの設定を変える必要があります。
1. [Product] > [Scheme] > [Edit Scheme...]を選択
2. 下の画像のような画面になるので[Option]を選択し、その中の[Store Kit Configuration]に先ほど作成したファイルを選択する

![スクリーンショット 2023-11-03 20.23.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1193085/cf77fc98-e0e9-b424-3353-899f5c903af0.png)

 ここまでStore Kitを使用する準備ができました。
 次にいよいよ実装を行なっていきます

## 4. 実装

ここでは順を追って説明していきます

### 商品情報の取得
まず登録した商品情報を取得していきます。
こちらだけなら数行のコードで行うことができます。

``` swift
import StoreKit

let productIds = ["[プロダクトID1]", "[プロダクトID2]", "[プロダクトID3]"]
let products = try await Product.products(for: productIds)
```

`products`には`productId`で登録した商品情報が配列で入っています。
なので例えばこの`products`をForEachなどで回してあげるだけで商品情報は取得できるというわけです。

### 商品の購入

上で商品情報の取得ができたので、次に商品を購入する方法を記載していきます。
なんらかのアクションがあった時に購入する処理は以下のようになっています。

``` swift
private func purchase(_ product: Product) async throws {
    let result = try await product.purchase()
}
```

上記のコードでは、`purchase`というメソッドがコールされると、引数で渡されたproductの購入処理が行われる、というものです。
ただもちろん、購入処理は例外が投げられるケースも存在します。
なので、そこはきちんとエラーハンドリングを実装するようにしましょう
``` swift
private func purchase(_ product: Product) async throws {
    let result = try await product.purchase()

    switch result {
    case let .success(.verified(transaction)):
        // 購入処理成功
        await transaction.finish()
    case let .success(.unverified(_, error)):
        // 購入は成功したが、StoreKitの自動検証チェックに失敗
        break
    case .pending:
        // SCA（強力な顧客認証）で待機中のトランザクション
        break
    case .userCancelled:
        // ユーザ側でキャンセル
        break
    @unknown default:
        break
    }
}
```

### 有料機能のロックの解除
課金を行うと当然、　有料機能のロックを解除するかどうかのロジックが必要となります。
これらはStoreKitの`Transaction.currentEntitlements`という機能を用いて実装していきます
例えば以下のようにすると、購入されている商品を取り出すことができます

``` swift
class PurchaseManager: ObservableObject {

    ...

    @Published
    private(set) var purchasedProductIDs = Set<String>()

    var hasUnlockedPro: Bool {
       return !self.purchasedProductIDs.isEmpty
    }

    func updatePurchasedProducts() async {
        for await result in Transaction.currentEntitlements {
            guard case .verified(let transaction) = result else {
                continue
            }

            if transaction.revocationDate == nil {
                self.purchasedProductIDs.insert(transaction.productID)
            } else {
                self.purchasedProductIDs.remove(transaction.productID)
            }
        }
    }
}

```

これで、購入されている商品は`purchasedProductIDs`というSetに格納されていきます。
これらはユーザがアプリケーションを起動する際に呼び出されると、課金しているユーザは有料機能を使用できるように実装することができます

``` swift
struct YourApp: App {
    @StateObject
    private var purchaseManager = PurchaseManager()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(purchaseManager)
                .task {
                    await purchaseManager.updatePurchasedProducts()
                }
        }
    }
}

```

### 購入の復元処理
アプリ内課金のあるiOSアプリは明示的にアプリ内で購入情報を復元処理を実装する必要があります
これをしないとApple Store Connectの審査に通らないからです。

やることとしてはApp Storeの情報と同期し、ユーザのApple Idで購入情報があるかどうかを実装する必要があります

``` swift
private func syncWithAppStore() async throws -> Bool {
        do {
            // AppStore.sync()の非同期操作を呼び出し
            try await AppStore.sync()
            return true
        } catch {
            // AppStore.sync()がエラーをスローした場合の処理
            throw error
        }
    }
```

これで一通りの課金周りの実装を行えるはずです。
課金まわりはセキュアに保つ必要があるため、本記事で説明した箇所以上に実装する必要があると思いますが、基本が商品情報の取得、購入、復元だと思います。
さらに詳細に知りたい方は英語ではありますが、本記事の参考文献をご参照ください〜

# さいごに
swiftを初めて約三ヶ月の私ですが、Chat AIの力を借りながらなんとかリリースまでいくことができました。
Chat AIではわからないところの質問だけでなく、綺麗なコードを書くための手助けもしてくれるので、今後は開発に積極的に取り入れていこうとおもいました
(皆さんもよかったら本アプリをつかってみてください〜、iOSだけでなく、macでも使用できます)

# 参考文献
https://www.revenuecat.com/blog/engineering/ios-in-app-subscription-tutorial-with-storekit-2-and-swift/?utm_source=google&utm_medium=cpc&utm_campaign=Alpha&utm_content=%22storekit%202%22&utm_ad=647866164209&utm_term=storekit%202%20tutorial&matchtype=e&device=c&GeoLoc=9157995&placement=&network=g&campaign_id=17824399608&adset_id=146872078238&ad_id=647866164209&gclid=Cj0KCQjwr82iBhCuARIsAO0EAZwlbFTzo0GHDHw8HcYfzkxlE33JH_cAK9Pe0Khc3ceW6GUm2hO2aSIaAuaQEALw_wcB