-
Meet StoreKit for SwiftUI
Discover how you can use App Store product metadata and Xcode Previews to add in-app purchases to your app with just a few lines of code. Explore a new collection of UI components in StoreKit and learn how you can easily merchandise your products, present subscriptions in a way that helps users make informed decisions, and more.
Recursos
Videos relacionados
WWDC23
- What’s new in App Store Connect
- What’s new in App Store server APIs
- What’s new in StoreKit 2 and StoreKit Testing in Xcode
- What’s new in SwiftUI
WWDC22
WWDC21
WWDC20
-
Buscar este video…
-
-
3:35 - Setting up the bird food shop view
import SwiftUI struct BirdFoodShop: View { var body: some View { Text("Hello, world!") } } -
3:42 - Import StoreKit to use the new merchandising views with SwiftUI
import SwiftUI import StoreKit struct BirdFoodShop: View { var body: some View { Text("Hello, world!") } } -
3:51 - Declaring a query to access the bird food data model
import SwiftUI import StoreKit struct BirdFoodShop: View { @Query var birdFood: [BirdFood] var body: some View { Text("Hello, world!") } } -
4:18 - Meet store view
import SwiftUI import StoreKit struct BirdFoodShop: View { @Query var birdFood: [BirdFood] var body: some View { StoreView(ids: birdFood.productIDs) } } -
4:51 - Adding decorative icons to the store view
import SwiftUI import StoreKit struct BirdFoodShop: View { @Query var birdFood: [BirdFood] var body: some View { StoreView(ids: birdFood.productIDs) { product in BirdFoodProductIcon(productID: product.id) } } } -
6:38 - Creating a container for a custom store layout
import SwiftUI import StoreKit struct BirdFoodShop: View { @Query var birdFood: [BirdFood] var body: some View { ScrollView { VStack(spacing: 10) { if let (birdFood, product) = birdFood.bestValue { } } .scrollClipDisabled() } .contentMargins(.horizontal, 20, for: .scrollContent) .scrollIndicators(.hidden) .frame(maxWidth: .infinity) .background(.background.secondary) } } -
6:47 - Meet product view
import SwiftUI import StoreKit struct BirdFoodShop: View { @Query var birdFood: [BirdFood] var body: some View { ScrollView { VStack(spacing: 10) { if let (birdFood, product) = birdFood.bestValue { ProductView(id: product.id) } } .scrollClipDisabled() } .contentMargins(.horizontal, 20, for: .scrollContent) .scrollIndicators(.hidden) .frame(maxWidth: .infinity) .background(.background.secondary) } } -
7:03 - Adding a decorative icon to the product view
import SwiftUI import StoreKit struct BirdFoodShop: View { @Query var birdFood: [BirdFood] var body: some View { ScrollView { VStack(spacing: 10) { if let (birdFood, product) = birdFood.bestValue { ProductView(id: product.id) { BirdFoodProductIcon( birdFood: birdFood, quantity: product.quantity ) } } } .scrollClipDisabled() } .contentMargins(.horizontal, 20, for: .scrollContent) .scrollIndicators(.hidden) .frame(maxWidth: .infinity) .background(.background.secondary) } } -
7:17 - Adding more containers to layout product views
import SwiftUI import StoreKit struct BirdFoodShop: View { @Query var birdFood: [BirdFood] var body: some View { ScrollView { VStack(spacing: 10) { if let (birdFood, product) = birdFood.bestValue { ProductView(id: product.id) { BirdFoodProductIcon( birdFood: birdFood, quantity: product.quantity ) } .padding() .background(.background.secondary, in: .rect(cornerRadius: 20)) } } .scrollClipDisabled() Text("Other Bird Food") .font(.title3.weight(.medium)) .frame(maxWidth: .infinity, alignment: .leading) ForEach(birdFood.premiumBirdFood) { birdFood in BirdFoodShopShelf(title: birdFood.name) { } } } .contentMargins(.horizontal, 20, for: .scrollContent) .scrollIndicators(.hidden) .frame(maxWidth: .infinity) .background(.background.secondary) } } -
7:36 - Declaring product views for the remaining products
import SwiftUI import StoreKit struct BirdFoodShop: View { @Query var birdFood: [BirdFood] var body: some View { ScrollView { VStack(spacing: 10) { if let (birdFood, product) = birdFood.bestValue { ProductView(id: product.id) { BirdFoodProductIcon( birdFood: birdFood, quantity: product.quantity ) } .padding() .background(.background.secondary, in: .rect(cornerRadius: 20)) } } .scrollClipDisabled() Text("Other Bird Food") .font(.title3.weight(.medium)) .frame(maxWidth: .infinity, alignment: .leading) ForEach(birdFood.premiumBirdFood) { birdFood in BirdFoodShopShelf(title: birdFood.name) { ForEach(birdFood.orderedProducts) { product in ProductView(id: product.id) { BirdFoodProductIcon( birdFood: birdFood, quantity: product.quantity ) } } } } } .contentMargins(.horizontal, 20, for: .scrollContent) .scrollIndicators(.hidden) .frame(maxWidth: .infinity) .background(.background.secondary) } } -
7:50 - Choosing a product view style
import SwiftUI import StoreKit struct BirdFoodShop: View { @Query var birdFood: [BirdFood] var body: some View { ScrollView { VStack(spacing: 10) { if let (birdFood, product) = birdFood.bestValue { ProductView(id: product.id) { BirdFoodProductIcon( birdFood: birdFood, quantity: product.quantity ) } .padding() .background(.background.secondary, in: .rect(cornerRadius: 20)) .padding() .productViewStyle(.large) } } .scrollClipDisabled() Text("Other Bird Food") .font(.title3.weight(.medium)) .frame(maxWidth: .infinity, alignment: .leading) ForEach(birdFood.premiumBirdFood) { birdFood in BirdFoodShopShelf(title: birdFood.name) { ForEach(birdFood.orderedProducts) { product in ProductView(id: product.id) { BirdFoodProductIcon( birdFood: birdFood, quantity: product.quantity ) } } } } } .contentMargins(.horizontal, 20, for: .scrollContent) .scrollIndicators(.hidden) .frame(maxWidth: .infinity) .background(.background.secondary) } } -
8:25 - Styling the store view
StoreView(ids: birdFood.productIDs) { product in BirdFoodShopIcon(productID: product.id) } .productViewStyle(.compact) -
9:53 - Setting up the Backyard Birds pass shop
import SwiftUI import StoreKit struct BackyardBirdsPassShop: View { var body: some View { Text("Hello, world!") } } -
9:57 - Meet subscription store view
import SwiftUI import StoreKit struct BackyardBirdsPassShop: View { @Environment(\.shopIDs.pass) var passGroupID var body: some View { SubscriptionStoreView(groupID: passGroupID) } } -
10:38 - Customizing the subscription store view's marketing content
import SwiftUI import StoreKit struct BackyardBirdsPassShop: View { @Environment(\.shopIDs.pass) var passGroupID var body: some View { SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() } } } -
10:57 - Declaring a full height container background
import SwiftUI import StoreKit struct BackyardBirdsPassShop: View { @Environment(\.shopIDs.pass) var passGroupID var body: some View { SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .lightMarketingContentStyle() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } } } -
11:21 - Configuring the control background style
import SwiftUI import StoreKit struct BackyardBirdsPassShop: View { @Environment(\.shopIDs.pass) var passGroupID var body: some View { SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .lightMarketingContentStyle() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .backgroundStyle(.clear) } } -
11:44 - Choosing a subscribe button label layout
import SwiftUI import StoreKit struct BackyardBirdsPassShop: View { @Environment(\.shopIDs.pass) var passGroupID var body: some View { SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .lightMarketingContentStyle() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .backgroundStyle(.clear) .subscriptionStoreButtonLabel(.multiline) } } -
12:01 - Choosing a subscription store picker item background
import SwiftUI import StoreKit struct BackyardBirdsPassShop: View { @Environment(\.shopIDs.pass) var passGroupID var body: some View { SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .lightMarketingContentStyle() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .backgroundStyle(.clear) .subscriptionStoreButtonLabel(.multiline) .subscriptionStorePicketItemBackground(.thinMaterial) } } -
12:20 - Declaring a redeem code button
import SwiftUI import StoreKit struct BackyardBirdsPassShop: View { @Environment(\.shopIDs.pass) var passGroupID var body: some View { SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .lightMarketingContentStyle() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .backgroundStyle(.clear) .subscriptionStoreButtonLabel(.multiline) .subscriptionStorePicketItemBackground(.thinMaterial) .storeButton(.visible, for: .redeemCode) } } -
14:10 - Reacting to completed purchases from descendant views
BirdFoodShop() .onInAppPurchaseCompletion { (product: Product, result: Result<Product.PurchaseResult, Error>) in if case .success(.success(let transaction)) = result { await BirdBrain.shared.process(transaction: transaction) dismiss() } } -
15:43 - Reacting to in-app purchases starting
BirdFoodShop() .onInAppPurchaseStart { (product: Product) in self.isPurchasing = true } -
16:57 - Declaring a subscription status dependency
subscriptionStatusTask(for: passGroupID) { taskState in if let statuses = taskState.value { passStatus = await BirdBrain.shared.status(for: statuses) } } -
19:37 - Unlocking non-consumables
currentEntitlementTask(for: "com.example.id") { state in self.isPurchased = BirdBrain.shared.isPurchased( for: state.transaction ) } -
20:52 - Declaring placeholder icons
ProductView(id: ids.nutritionPelletBox) { BoxOfNutritionPelletsIcon() } placeholderIcon: { Circle() } -
21:25 - Using the promotional icon
ProductView( id: ids.nutritionPelletBox, prefersPromotionalIcon: true ) { BoxOfNutritionPelletsIcon() } -
21:56 - Using the promotional icon border
ProductView(id: ids.nutritionPelletBox) { BoxOfNutritionPelletsIcon() .productIconBorder() } -
23:02 - Composing standard styles to create custom styles
struct SpinnerWhenLoadingStyle: ProductViewStyle { func makeBody(configuration: Configuration) -> some View { switch configuration.state { case .loading: ProgressView() .progressViewStyle(.circular) default: ProductView(configuration) } } } -
23:44 - Applying custom styles to the product view
ProductView(id: ids.nutritionPelletBox) { BoxOfNutritionPelletsIcon() } .productViewStyle(SpinnerWhenLoadingStyle()) -
23:58 - Declaring custom styles
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() } } } -
26:44 - Declaring a dependency on products
@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 } } -
27:54 - Configuring the visibility of auxiliary buttons
SubscriptionStoreView(groupID: passGroupID) { // ... } .storeButton(.visible, for: .redeemCode) -
29:56 - Adding a sign in action
@State var presentingSignInSheet = false var body: some View { SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .subscriptionStoreSignInAction { presentingSignInSheet = true } .sheet(isPresented: $presentingSignInSheet) { SignInToBirdAccountView() } } -
30:32 - Displaying policies from the App Store metadata
SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .subscriptionStorePolicyForegroundStyle(.white) .storeButton(.visible, for: .policies) -
31:22 - Choosing a control style
SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .subscriptionStoreControlStyle(.buttons) -
32:28 - Declaring the layout of the subscribe button label
SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .subscriptionStoreButtonLabel(.multiline) -
32:51 - Declaring the content of the subscribe button label
SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .subscriptionStoreButtonLabel(.displayName) -
33:04 - Declaring the layout and content of the subscribe button label
SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .subscriptionStoreButtonLabel(.multiline.displayName) -
33:44 - Decorating subscription plans
SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .subscriptionStoreControlIcon { subscription, info in Group { let status = PassStatus( levelOfService: info.groupLevel ) switch status { case .premium: Image(systemName: "bird") case .family: Image(systemName: "person.3.sequence") default: Image(systemName: "wallet.pass") } } .foregroundStyle(.tint) .symbolVariant(.fill) } -
34:07 - Decorating subscription plans with the button control style
SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .subscriptionStoreControlIcon { subscription, info in Group { let status = PassStatus( levelOfService: info.groupLevel ) switch status { case .premium: Image(systemName: "bird") case .family: Image(systemName: "person.3.sequence") default: Image(systemName: "wallet.pass") } } .symbolVariant(.fill) } .foregroundStyle(.white) .subscriptionStoreControlStyle(.buttons) -
34:14 - Adding a container background
SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .containerBackground( .accent.gradient, for: .subscriptionStore ) } -
35:30 - Presenting upgrade offers
SubscriptionStoreView( groupID: passGroupID, visibleRelationships: .upgrade ) { PremiumMarketingContent() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } }
-