2023 App Store, Distribution & MarketingSwiftUI & UI FrameworksApp Services
WWDC23 · 37 min · App Store, Distribution & Marketing / SwiftUI & UI Frameworks / App Services
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.
Watch at developer.apple.com ↗Code shown on screen · 42 snippets
Setting up the bird food shop view
import SwiftUI
struct BirdFoodShop: View {
var body: some View {
Text("Hello, world!")
}
} Import StoreKit to use the new merchandising views with SwiftUI
import SwiftUI
import StoreKit
struct BirdFoodShop: View {
var body: some View {
Text("Hello, world!")
}
} Declaring a query to access the bird food data model
import SwiftUI
import StoreKit
struct BirdFoodShop: View {
var birdFood: [BirdFood]
var body: some View {
Text("Hello, world!")
}
} Meet store view
import SwiftUI
import StoreKit
struct BirdFoodShop: View {
var birdFood: [BirdFood]
var body: some View {
StoreView(ids: birdFood.productIDs)
}
} Adding decorative icons to the store view
import SwiftUI
import StoreKit
struct BirdFoodShop: View {
var birdFood: [BirdFood]
var body: some View {
StoreView(ids: birdFood.productIDs) { product in
BirdFoodProductIcon(productID: product.id)
}
}
} Creating a container for a custom store layout
import SwiftUI
import StoreKit
struct BirdFoodShop: View {
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)
}
} Meet product view
import SwiftUI
import StoreKit
struct BirdFoodShop: View {
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)
}
} Adding a decorative icon to the product view
import SwiftUI
import StoreKit
struct BirdFoodShop: View {
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)
}
} Adding more containers to layout product views
import SwiftUI
import StoreKit
struct BirdFoodShop: View {
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)
}
} Declaring product views for the remaining products
import SwiftUI
import StoreKit
struct BirdFoodShop: View {
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)
}
} Choosing a product view style
import SwiftUI
import StoreKit
struct BirdFoodShop: View {
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)
}
} Styling the store view
StoreView(ids: birdFood.productIDs) { product in
BirdFoodShopIcon(productID: product.id)
}
.productViewStyle(.compact) Setting up the Backyard Birds pass shop
import SwiftUI
import StoreKit
struct BackyardBirdsPassShop: View {
var body: some View {
Text("Hello, world!")
}
} Meet subscription store view
import SwiftUI
import StoreKit
struct BackyardBirdsPassShop: View {
(\.shopIDs.pass) var passGroupID
var body: some View {
SubscriptionStoreView(groupID: passGroupID)
}
} Customizing the subscription store view's marketing content
import SwiftUI
import StoreKit
struct BackyardBirdsPassShop: View {
(\.shopIDs.pass) var passGroupID
var body: some View {
SubscriptionStoreView(groupID: passGroupID) {
PassMarketingContent()
}
}
} Declaring a full height container background
import SwiftUI
import StoreKit
struct BackyardBirdsPassShop: View {
(\.shopIDs.pass) var passGroupID
var body: some View {
SubscriptionStoreView(groupID: passGroupID) {
PassMarketingContent()
.lightMarketingContentStyle()
.containerBackground(for: .subscriptionStoreFullHeight) {
SkyBackground()
}
}
}
} Configuring the control background style
import SwiftUI
import StoreKit
struct BackyardBirdsPassShop: View {
(\.shopIDs.pass) var passGroupID
var body: some View {
SubscriptionStoreView(groupID: passGroupID) {
PassMarketingContent()
.lightMarketingContentStyle()
.containerBackground(for: .subscriptionStoreFullHeight) {
SkyBackground()
}
}
.backgroundStyle(.clear)
}
} Choosing a subscribe button label layout
import SwiftUI
import StoreKit
struct BackyardBirdsPassShop: View {
(\.shopIDs.pass) var passGroupID
var body: some View {
SubscriptionStoreView(groupID: passGroupID) {
PassMarketingContent()
.lightMarketingContentStyle()
.containerBackground(for: .subscriptionStoreFullHeight) {
SkyBackground()
}
}
.backgroundStyle(.clear)
.subscriptionStoreButtonLabel(.multiline)
}
} Choosing a subscription store picker item background
import SwiftUI
import StoreKit
struct BackyardBirdsPassShop: View {
(\.shopIDs.pass) var passGroupID
var body: some View {
SubscriptionStoreView(groupID: passGroupID) {
PassMarketingContent()
.lightMarketingContentStyle()
.containerBackground(for: .subscriptionStoreFullHeight) {
SkyBackground()
}
}
.backgroundStyle(.clear)
.subscriptionStoreButtonLabel(.multiline)
.subscriptionStorePicketItemBackground(.thinMaterial)
}
} Declaring a redeem code button
import SwiftUI
import StoreKit
struct BackyardBirdsPassShop: View {
(\.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)
}
} 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()
}
} Reacting to in-app purchases starting
BirdFoodShop()
.onInAppPurchaseStart { (product: Product) in
self.isPurchasing = true
} Declaring a subscription status dependency
subscriptionStatusTask(for: passGroupID) { taskState in
if let statuses = taskState.value {
passStatus = await BirdBrain.shared.status(for: statuses)
}
} Unlocking non-consumables
currentEntitlementTask(for: "com.example.id") { state in
self.isPurchased = BirdBrain.shared.isPurchased(
for: state.transaction
)
} Declaring placeholder icons
ProductView(id: ids.nutritionPelletBox) {
BoxOfNutritionPelletsIcon()
} placeholderIcon: {
Circle()
} Using the promotional icon
ProductView(
id: ids.nutritionPelletBox,
prefersPromotionalIcon: true
) {
BoxOfNutritionPelletsIcon()
} Using the promotional icon border
ProductView(id: ids.nutritionPelletBox) {
BoxOfNutritionPelletsIcon()
.productIconBorder()
} 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)
}
}
} Applying custom styles to the product view
ProductView(id: ids.nutritionPelletBox) {
BoxOfNutritionPelletsIcon()
}
.productViewStyle(SpinnerWhenLoadingStyle()) 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()
}
}
} Declaring a dependency on products
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
}
} Configuring the visibility of auxiliary buttons
SubscriptionStoreView(groupID: passGroupID) {
// ...
}
.storeButton(.visible, for: .redeemCode) Adding a sign in action
var presentingSignInSheet = false
var body: some View {
SubscriptionStoreView(groupID: passGroupID) {
PassMarketingContent()
.containerBackground(for: .subscriptionStoreFullHeight) {
SkyBackground()
}
}
.subscriptionStoreSignInAction {
presentingSignInSheet = true
}
.sheet(isPresented: $presentingSignInSheet) {
SignInToBirdAccountView()
}
} Displaying policies from the App Store metadata
SubscriptionStoreView(groupID: passGroupID) {
PassMarketingContent()
.containerBackground(for: .subscriptionStoreFullHeight) {
SkyBackground()
}
}
.subscriptionStorePolicyForegroundStyle(.white)
.storeButton(.visible, for: .policies) Choosing a control style
SubscriptionStoreView(groupID: passGroupID) {
PassMarketingContent()
.containerBackground(for: .subscriptionStoreFullHeight) {
SkyBackground()
}
}
.subscriptionStoreControlStyle(.buttons) Declaring the layout of the subscribe button label
SubscriptionStoreView(groupID: passGroupID) {
PassMarketingContent()
.containerBackground(for: .subscriptionStoreFullHeight) {
SkyBackground()
}
}
.subscriptionStoreButtonLabel(.multiline) Declaring the content of the subscribe button label
SubscriptionStoreView(groupID: passGroupID) {
PassMarketingContent()
.containerBackground(for: .subscriptionStoreFullHeight) {
SkyBackground()
}
}
.subscriptionStoreButtonLabel(.displayName) Declaring the layout and content of the subscribe button label
SubscriptionStoreView(groupID: passGroupID) {
PassMarketingContent()
.containerBackground(for: .subscriptionStoreFullHeight) {
SkyBackground()
}
}
.subscriptionStoreButtonLabel(.multiline.displayName) 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)
} 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) Adding a container background
SubscriptionStoreView(groupID: passGroupID) {
PassMarketingContent()
.containerBackground(
.accent.gradient,
for: .subscriptionStore
)
} Presenting upgrade offers
SubscriptionStoreView(
groupID: passGroupID,
visibleRelationships: .upgrade
) {
PremiumMarketingContent()
.containerBackground(for: .subscriptionStoreFullHeight) {
SkyBackground()
}
} Resources
Related sessions
-
13 min -
24 min -
21 min -
34 min -
35 min -
38 min -
25 min