Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

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 ↗

Transcript all transcripts

Code shown on screen · 42 snippets

Setting up the bird food shop view swift · at 3:35 ↗
import SwiftUI

struct BirdFoodShop: View {
 
  var body: some View {
    Text("Hello, world!") 
  }
  
}
Import StoreKit to use the new merchandising views with SwiftUI swift · at 3:42 ↗
import SwiftUI
import StoreKit

struct BirdFoodShop: View {
 
  var body: some View {
    Text("Hello, world!") 
  }
  
}
Declaring a query to access the bird food data model swift · at 3:51 ↗
import SwiftUI
import StoreKit

struct BirdFoodShop: View {
  @Query var birdFood: [BirdFood]
 
  var body: some View {
    Text("Hello, world!") 
  }
  
}
Meet store view swift · at 4:18 ↗
import SwiftUI
import StoreKit

struct BirdFoodShop: View {
  @Query var birdFood: [BirdFood]
 
  var body: some View {
    StoreView(ids: birdFood.productIDs) 
  }
  
}
Adding decorative icons to the store view swift · at 4:51 ↗
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)
    }
  }
 
}
Creating a container for a custom store layout swift · at 6:38 ↗
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)  
  }
  
}
Meet product view swift · at 6:47 ↗
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)  
  }
  
}
Adding a decorative icon to the product view swift · at 7:03 ↗
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)  
  }
  
}
Adding more containers to layout product views swift · at 7:17 ↗
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)  
  }
  
}
Declaring product views for the remaining products swift · at 7:36 ↗
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)  
  }
  
}
Choosing a product view style swift · at 7:50 ↗
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)  
  }
  
}
Styling the store view swift · at 8:25 ↗
StoreView(ids: birdFood.productIDs) { product in 
    BirdFoodShopIcon(productID: product.id)
}
.productViewStyle(.compact)
Setting up the Backyard Birds pass shop swift · at 9:53 ↗
import SwiftUI
import StoreKit

struct BackyardBirdsPassShop: View {
 
    var body: some View {
        Text("Hello, world!") 
    }
  
}
Meet subscription store view swift · at 9:57 ↗
import SwiftUI
import StoreKit

struct BackyardBirdsPassShop: View {
    @Environment(\.shopIDs.pass) var passGroupID
 
    var body: some View {
        SubscriptionStoreView(groupID: passGroupID)
    }
  
}
Customizing the subscription store view's marketing content swift · at 10:38 ↗
import SwiftUI
import StoreKit

struct BackyardBirdsPassShop: View {
    @Environment(\.shopIDs.pass) var passGroupID
 
    var body: some View {
        SubscriptionStoreView(groupID: passGroupID) {
            PassMarketingContent() 
        }
    }
  
}
Declaring a full height container background swift · at 10:57 ↗
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()
                }
        }
    }
  
}
Configuring the control background style swift · at 11:21 ↗
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)
    }
  
}
Choosing a subscribe button label layout swift · at 11:44 ↗
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)
    }
  
}
Choosing a subscription store picker item background swift · at 12:01 ↗
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)
    }
  
}
Declaring a redeem code button swift · at 12:20 ↗
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)
    }
  
}
Reacting to completed purchases from descendant views swift · at 14:10 ↗
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 swift · at 15:43 ↗
BirdFoodShop()
    .onInAppPurchaseStart { (product: Product) in
        self.isPurchasing = true
    }
Declaring a subscription status dependency swift · at 16:57 ↗
subscriptionStatusTask(for: passGroupID) { taskState in
    if let statuses = taskState.value {
        passStatus = await BirdBrain.shared.status(for: statuses)
    }            
}
Unlocking non-consumables swift · at 19:37 ↗
currentEntitlementTask(for: "com.example.id") { state in
    self.isPurchased = BirdBrain.shared.isPurchased(
        for: state.transaction
    )
}
Declaring placeholder icons swift · at 20:52 ↗
ProductView(id: ids.nutritionPelletBox) {
    BoxOfNutritionPelletsIcon()
} placeholderIcon: {
    Circle()
}
Using the promotional icon swift · at 21:25 ↗
ProductView(
    id: ids.nutritionPelletBox,
    prefersPromotionalIcon: true
) {
    BoxOfNutritionPelletsIcon()
}
Using the promotional icon border swift · at 21:56 ↗
ProductView(id: ids.nutritionPelletBox) {
    BoxOfNutritionPelletsIcon()
        .productIconBorder()
}
Composing standard styles to create custom styles swift · at 23:02 ↗
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 swift · at 23:44 ↗
ProductView(id: ids.nutritionPelletBox) {
    BoxOfNutritionPelletsIcon()
}
.productViewStyle(SpinnerWhenLoadingStyle())
Declaring custom styles swift · at 23:58 ↗
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 swift · at 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
    }
}
Configuring the visibility of auxiliary buttons swift · at 27:54 ↗
SubscriptionStoreView(groupID: passGroupID) {
   // ...
}
.storeButton(.visible, for: .redeemCode)
Adding a sign in action swift · at 29:56 ↗
@State 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 swift · at 30:32 ↗
SubscriptionStoreView(groupID: passGroupID) {
    PassMarketingContent()
        .containerBackground(for: .subscriptionStoreFullHeight) {
            SkyBackground()
        }
}
.subscriptionStorePolicyForegroundStyle(.white)
.storeButton(.visible, for: .policies)
Choosing a control style swift · at 31:22 ↗
SubscriptionStoreView(groupID: passGroupID) {
    PassMarketingContent()
        .containerBackground(for: .subscriptionStoreFullHeight) {
            SkyBackground()
        }
}
.subscriptionStoreControlStyle(.buttons)
Declaring the layout of the subscribe button label swift · at 32:28 ↗
SubscriptionStoreView(groupID: passGroupID) {
    PassMarketingContent()
        .containerBackground(for: .subscriptionStoreFullHeight) {
            SkyBackground()
        }
}
.subscriptionStoreButtonLabel(.multiline)
Declaring the content of the subscribe button label swift · at 32:51 ↗
SubscriptionStoreView(groupID: passGroupID) {
    PassMarketingContent()
        .containerBackground(for: .subscriptionStoreFullHeight) {
            SkyBackground()
        }
}
.subscriptionStoreButtonLabel(.displayName)
Declaring the layout and content of the subscribe button label swift · at 33:04 ↗
SubscriptionStoreView(groupID: passGroupID) {
    PassMarketingContent()
        .containerBackground(for: .subscriptionStoreFullHeight) {
            SkyBackground()
        }
}
.subscriptionStoreButtonLabel(.multiline.displayName)
Decorating subscription plans swift · at 33:44 ↗
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 swift · at 34:07 ↗
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 swift · at 34:14 ↗
SubscriptionStoreView(groupID: passGroupID) {
    PassMarketingContent()
        .containerBackground(
            .accent.gradient,
            for: .subscriptionStore
        )
}
Presenting upgrade offers swift · at 35:30 ↗
SubscriptionStoreView(
    groupID: passGroupID,
    visibleRelationships: .upgrade
) {
    PremiumMarketingContent()
        .containerBackground(for: .subscriptionStoreFullHeight) {
            SkyBackground()
        }
}

Resources