この記事は集まれSwift好き!Swift愛好会スピンオフ WWDC23セッション要約会 @ DeNAの発表で使用したものです
発表内容は、以下のセッション内容についてのものです
2021年の時もStoreKit2について、集まれSwift好き!Swift愛好会スピンオフ WWDC21セッション要約会 @オンラインで発表していたので、それに関連してこのセッションを選択しました
StoreKit2について確認したい方は以下の記事を見てみてください
StoreKitもどんどんアップデートされて、今まで実装や仕組みが複雑だったiOSの課金機能の実装もどんどんハードルが下がるようになってきました
SwiftUIでStoreKitのUIを実装する
セッションのポイント
上記のセッションの内容のポイントは以下の通りです
- StoreKitの標準のSwiftUIで宣言的にUIをかけるようになり、素早く課金機能を実装できるようになった
- 標準のUIとはいえ、かなりカスタマイズすることができる
- サブスクリプション用のUIも提供されている
- Previewで動作確認をすることができる
- この機能はAppleの全てのプラットフォームに対応している
例の如く、今年もサンプルアプリのソースコードが公開されており、そちらでセッションで紹介されていたソースコードの詳細も確認することができます
sample-backyard-birds
主な登場人物
今回追加されたViewは大きく以下の3種類です
- StoreView
- 課金アイテムの一覧を表示するView
- ProductView
- 各課金アイテムごとにカスタムでレイアウトを変更したい場合に使用するView
- 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のButton
のButtonStyle
を使った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.
https://developer.apple.com/documentation/deviceactivity/deviceactivityreport/containerbackground(_:for:)ContainerBackgroundPlacement
describes the available containers.
.backgroundStyle(.clear)
はメニューのUI自体の背景をclearにするように設定しています(上記のcontainerBackground
で指定したものが背景になる)
subscriptionStorePicketItemBackgroundはメニューの各選択肢のViewの背景を指定するもので、この場合は、thinMaterial
を指定しているので、iOSアプリでよく見るすりガラスのようなデザインになります
またこのViewはUIだけでなく、以下の制御も行ってくれます
- 購入後のコンテンツの解放
- 購入後に購入ボタンを非活性にする
そして以下のように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つが紹介されています
- cancellation
- サブスクリプションのキャンセル用
- restorePurchase
- redeemCode
- いわゆるオファーコードを入力するためのもの
- signIn
- 追加でボタンタップ時に呼ばれる
subscriptionStoreSignInAction
Modifierの実装が必要
- 追加でボタンタップ時に呼ばれる
- policies
- Terms of ServiceやPrivacy Policyのボタンを表示
まとめ
ここまでStoreKitの機能をSwiftUIの標準のViewで実装する方法について、確認してきました
この機能については、以下のようなメリットがあるように感じます
- コアな機能の開発に集中することができる
- かなり多種多様なModifierなども用意されていて、かなりカスタマイズしやすい
- Appleのデザインガイドラインに沿ったUIが簡単に実装できる
- UIを作り込む際に、複雑な対応はプラットフォーム側に任せることができる
引用: https://developer.apple.com/videos/play/wwdc2023/10013/
ただ一方で以下のようなデメリットや注意点も発生しそうです
- 複雑なレイアウトや機能を実現したい場合は機能として物足りないかも
- とはいえかなりカスタムできるので、よほど凝ったものを作りたい場合以外は、問題なさそう
- 標準で提供されすぎるが故に実際内部でどのようなことが行われているのかよくわからない(黒魔術っぽい感じ?)まま、とりあえず使ってしまう
- どのようなレイアウトのカスタマイズができるか、デザイナーと仕様や実装の仕方などを擦り合わせることが必要な場合もある
- 必要に応じて、エンジニアがデザインの段階からサポートに入るのもありかも
ただ確実に今回のこの機能追加によって、Appleプラットフォームでの課金機能の実装のハードルはより低くなったので、iOS17以降(遠い..)で実装する場合はこの機能を使って、スピーディーに実装してみてください!
なおこの情報はWWDC2023のセッション動画の情報を元に、beta版の内容をまとめたもので、正式リリース時はおそらく色々変更が入っていると思うので、その際はセッション動画やこの記事を参考に、最新情報を確認しながら、実装をお願いします!
皆さんもStoreKitを使って、課金機能を実装して、Appleにお布施を払いましょう!