Event

[WWDC2023]StoreKitの機能をSwiftUIで実装する

この記事は集まれSwift好き!Swift愛好会スピンオフ WWDC23セッション要約会 @ DeNAの発表で使用したものです

発表内容は、以下のセッション内容についてのものです

  1. Meet StoreKit for SwiftUI

2021年の時もStoreKit2について、集まれSwift好き!Swift愛好会スピンオフ WWDC21セッション要約会 @オンラインで発表していたので、それに関連してこのセッションを選択しました
StoreKit2について確認したい方は以下の記事を見てみてください

wwdc-2021-storekit2
[WWDC2021] StoreKit2の実装方法を確認するWWDC2021で発表されたStoreKit2のサンプルアプリを元に、StoreKit2での新しい課金処理の実装方法を確認します...

StoreKitもどんどんアップデートされて、今まで実装や仕組みが複雑だったiOSの課金機能の実装もどんどんハードルが下がるようになってきました

SwiftUIでStoreKitのUIを実装する

セッションのポイント

上記のセッションの内容のポイントは以下の通りです

SwiftUIでStoreKitの機能の実装
  1. StoreKitの標準のSwiftUIで宣言的にUIをかけるようになり、素早く課金機能を実装できるようになった
  2. 標準のUIとはいえ、かなりカスタマイズすることができる
  3. サブスクリプション用のUIも提供されている
  4. Previewで動作確認をすることができる
  5. この機能はAppleの全てのプラットフォームに対応している

例の如く、今年もサンプルアプリのソースコードが公開されており、そちらでセッションで紹介されていたソースコードの詳細も確認することができます
sample-backyard-birds

主な登場人物

今回追加されたViewは大きく以下の3種類です

  1. StoreView
    • 課金アイテムの一覧を表示するView
  2. ProductView
    • 各課金アイテムごとにカスタムでレイアウトを変更したい場合に使用するView
  3. SubscriptionStoreView
    • サブスクリプションのメニューやボタンを表示するView

StoreView

セッションでは、5:50~辺りから説明が始まります

以下のように、StoreKitViewに課金アイテムのID(String)の配列を渡すことで自動で、その課金アイテムの情報(表示名、詳細、金額)を表示したリストが表示されます

StoreView(ids: productIDs)

引用: https://developer.apple.com/videos/play/wwdc2023/10013/

アイコンを表示したい場合は、以下のように書くことで取得できます

StoreView(ids: productIDs) { product in 
    BirdFoodProductIcon(productID: product.id)
}

引用: https://developer.apple.com/videos/play/wwdc2023/10013/

StoreViewのパラメーターのViewBuilderの引数からProductを取得することができるので、それを使って画像を表示するViewを生成します(BirdFoodProductIconはただのImage(アイコン)を表示するものです)

ProductView

個別の課金アイテムのUIをカスタマイズしたい場合は、ProductViewを使用します(動画参照コード参照)

ScrollView {
    VStack(spacing: 10) {
        if let (birdFood, product) = bestValue {
            ProductView(id: product.id) {
                BirdFoodProductIcon(birdFood: birdFood, quantity: product.quantity)
                    .bestBirdFoodValueBadge()
            }
            .padding(.vertical)
            .background(.background.secondary, in: .rect(cornerRadius: 20))
            .productViewStyle(.large)
            .padding()

            // ~~omit~~
        }
        ForEach(premiumBirdFood) { birdFood in
            BirdFoodShopShelf(title: birdFood.name) {
                ForEach(birdFood.orderedProducts) { product in
                    ProductView(id: product.id) {
                        BirdFoodProductIcon(birdFood: birdFood, quantity: product.quantity)
                    }
                }
            }
        }
    }
    .scrollClipDisabled()
}
.contentMargins(.horizontal, 20, for: .scrollContent)
.scrollIndicators(.hidden)

上記の通り、背景色やpaddingなども使用できるので、柔軟にレイアウトを変更することができます

productViewStyleというのは、ProductViewの表示スタイルを切り替えるものです
①Compact, ②Regular, ③Largeの3つを選択できます(8:15~)

またProductViewのレイアウトは、productViewStyleのModifierでProductViewStyleに準拠したstructを設定することで自身でカスタムすることができます(これは通常のSwiftUIのButtonButtonStyleを使ったbuttonStyleのModifierと同じようなイメージです)

ProductView(id: ids.nutritionPelletBox)
  .productViewStyle(SpinnerWhenLoadingStyle())
struct SpinnerWhenLoadingStyle: ProductViewStyle {
    func makeBody(configuration: Configuration) -> some View {
        switch configuration.state {
        case .loading:
            ProgressView()
                .progressViewStyle(.circular)
        default:
            ProductView(configuration)
        }
    }
}

上記は課金アイテムの情報をLoading中にProgressViewを表示するためのものです

struct BackyardBirdsStyle: ProductViewStyle {
  func makeBody(configuration: Configuration) -> some View {
    switch configuration.state {
      case .loading: // Handle loading state here
      case .failure(let error): // Handle failure state here
      case .unavailable: // Handle unavailabiltity here
      case .success(let product):
        HStack(spacing: 12) {
          configuration.icon
          VStack(alignment: .leading, spacing: 10) {
            Text(product.displayName)
            Button(product.displayPrice) {
              configuration.purchase()
            }
            .bold()
          }
        }
        .backyardBirdsProductBackground()
    }
  }
}

こちらはLoadingが終わって、情報の取得が成功したときに表示するUIをカスタムするものです

SubscriptionStoreView

次はサブスクリプション用のViewです
サブスクリプションは一般の課金アイテムと比較して、実装が難しいですが、このViewを使うことで実装のハードルは大きく下げられると思います

SubscriptionStoreView(groupID: passGroupID) {
  PassMarketingContent()
    .containerBackground(for: .subscriptionStoreFullHeight) {
      SkyBackground()
    }
}
.backgroundStyle(.clear)
.subscriptionStorePicketItemBackground(.thinMaterial)
.storeButton(.visible, for: .redeemCode)

引用: https://developer.apple.com/videos/play/wwdc2023/10013/

今までと同じように、groupID(String)を渡すだけで、サブスクリプションのメニューと購入ボタンのUIが表示されます
ViewBuilderの引数で渡しているのは、メニュー上部に表示されるマーケティングコンテンツです(10:40~)

containerBackgroundはiOS17から利用できる背景のUIを指定できるものです
今回はsubscriptionStoreFullHeightを指定して、SubscriptionStoreViewの高さ全体に適用されるようにしています
ちなみに既存のBackgroundなどとの違いについては、ドキュメントには以下のように記載があります(サブスクリプションのViewやNavigationなど、特定の場面の背景の指定がしやすくなる?)

automatically filling an entire parent container. ContainerBackgroundPlacement describes the available containers.

https://developer.apple.com/documentation/deviceactivity/deviceactivityreport/containerbackground(_:for:)

.backgroundStyle(.clear)はメニューのUI自体の背景をclearにするように設定しています(上記のcontainerBackgroundで指定したものが背景になる)

subscriptionStorePicketItemBackgroundはメニューの各選択肢のViewの背景を指定するもので、この場合は、thinMaterialを指定しているので、iOSアプリでよく見るすりガラスのようなデザインになります

またこのViewはUIだけでなく、以下の制御も行ってくれます

  1. 購入後のコンテンツの解放
  2. 購入後に購入ボタンを非活性にする

そして以下のようにvisibleRelationships.upgradeを指定することで、より上位のサブスクリプションアイテムへのアップグレードへの導線も管理してくれます(35:30~)

SubscriptionStoreView(
    groupID: passGroupID,
    visibleRelationships: .upgrade
)

引用: https://developer.apple.com/videos/play/wwdc2023/10013/

その他色々なModifier

以下のように色々なModifierが追加されました
セッション動画でいうと、14:00~から確認できます

onInAppPurchaseCompletion

課金のproductとその結果を取得できます(サンプルコード)

.onInAppPurchaseCompletion { product, purchaseResult in
    guard case .success(let verificationResult) = purchaseResult,
          case .success(_) = verificationResult else {
        return
    }
    showingSubscriptionStore = false
}

結果を取得したを処理するコードは、StoreKit2で見覚えのあるものになっています

onInAppPurchaseStart

こちらは課金処理が始まったことを検知するためのModifierです

.onInAppPurchaseStart { (product: Product) in
  self.isPurchasing = true
}

subscriptionStatusTask

サブスクリプションのステータスを取得することができます
サブスクリプションの購入完了を検知して、画面を自動で閉じるなどの処理も実装できます

subscriptionStatusTask(for: passGroupID) { taskState in
    if let statuses = taskState.value {
        passStatus = await BirdBrain.shared.status(for: statuses)
    }            
}

storeProductsTask

課金情報を取得する際のStateを取得できます
取得中にLoadingのUIを表示したりすることができます(26:44~)

@State var productsState: Product.CollectionTaskState = .loading

var body: some View {
    ZStack {
        switch productsState {
        case .loading:
            BirdFoodShopLoadingView()
        case .failed(let error):
            ContentUnavailableView(/* ... */)
        case .success(let products, let unavailableIDs):
            if products.isEmpty {
                ContentUnavailableView(/* ... */)
            }
            else {
                BirdFoodShop(products: products)
            }
        }
    }
    .storeProductsTask(for: productIDs) { state in
        self.productsState = state
    }
}

storeButton

以下のように①Visibilityと②StoreButtonKindを指定することで、該当の機能のボタンの表示/非表示を制御できます
既述のサブスクリプションのリストアボタンのこのModifierを使用しています

.storeButton(.visible, for: .redeemCode)

セッションでは②のStoreButtonKindとして、以下の5つが紹介されています

  1. cancellation
    • サブスクリプションのキャンセル用
  2. restorePurchase
  3. redeemCode
    • いわゆるオファーコードを入力するためのもの
  4. signIn
    • 追加でボタンタップ時に呼ばれるsubscriptionStoreSignInActionModifierの実装が必要
  5. policies
    • Terms of ServicePrivacy Policyのボタンを表示

まとめ

ここまでStoreKitの機能をSwiftUIの標準のViewで実装する方法について、確認してきました
この機能については、以下のようなメリットがあるように感じます

  1. コアな機能の開発に集中することができる
  2. かなり多種多様なModifierなども用意されていて、かなりカスタマイズしやすい
  3. Appleのデザインガイドラインに沿ったUIが簡単に実装できる
  4. UIを作り込む際に、複雑な対応はプラットフォーム側に任せることができる
    • 多言語・多地域対応
      • 特にお金が関わるところなので、慎重な実装やテストが必要な機能になるため
    • アクセシビリティ
    • 複数のプラットフォーム対応
      • 例えばサブスクリプションのボタンのstyleのUIもプラットフォームごとに自動で調整される(31:45~)
      • VisionProでも動くのでは?
    • ScreenTimeによる課金有効/無効のチェック(参照)
    • 今後のSwiftUIを中心とした機能のアップデート

引用: https://developer.apple.com/videos/play/wwdc2023/10013/

ただ一方で以下のようなデメリットや注意点も発生しそうです

  1. 複雑なレイアウトや機能を実現したい場合は機能として物足りないかも
    • とはいえかなりカスタムできるので、よほど凝ったものを作りたい場合以外は、問題なさそう
  2. 標準で提供されすぎるが故に実際内部でどのようなことが行われているのかよくわからない(黒魔術っぽい感じ?)まま、とりあえず使ってしまう
  3. どのようなレイアウトのカスタマイズができるか、デザイナーと仕様や実装の仕方などを擦り合わせることが必要な場合もある
    • 必要に応じて、エンジニアがデザインの段階からサポートに入るのもありかも

ただ確実に今回のこの機能追加によって、Appleプラットフォームでの課金機能の実装のハードルはより低くなったので、iOS17以降(遠い..)で実装する場合はこの機能を使って、スピーディーに実装してみてください!

なおこの情報はWWDC2023のセッション動画の情報を元に、beta版の内容をまとめたもので、正式リリース時はおそらく色々変更が入っていると思うので、その際はセッション動画やこの記事を参考に、最新情報を確認しながら、実装をお願いします!

皆さんもStoreKitを使って、課金機能を実装して、Appleにお布施を払いましょう!

+3

COMMENT

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

CAPTCHA