Streaming is available in most browsers,
and in the Developer app.
-
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.
Resources
Related Videos
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
-
Download
Welcome to Meet StoreKit for SwiftUI. I'm Greg, an engineer on the StoreKit team. Let's talk about merchandising in-app purchases. Merchandising in-app purchase is all about presenting your product offerings and providing a way for customers to complete the purchase.
Merchandising starts with getting data about the product you're selling and getting the status of your customer. For example, does the customer already own my non-consumable product? Are they subscribed to my subscription? You combine this data to build an interface to market the product to customers and provide interactions to purchase the product. This little red rectangle underrepresents all the hard work that goes into building your interface. There are actually many aspects to building your interface, requiring skills from a variety of disciplines. Then, your customer chooses to purchase your product. Your app needs to respond by using the purchase API and then updating its interface as a result of the purchase. If you've ever added in-app purchase to an app, you know getting merchandising right is critical. Wouldn't it be nice if we could abstract all these steps into a simple, yet powerful, view? This view could handle all common functionality and take parameters which allow you to configure the bits that make your app, your app. Well, I'm excited to introduce a powerful new set of APIs from StoreKit for building merchandising UI. In Xcode 15, StoreKit now provides a collection of SwiftUI views, which help you build declarative in-app purchase UI. You just declare what you want the merchandising experience to be, and the system puts your declarations into action behind the scenes.
The StoreView, ProductView, and SubscriptionStoreView are new views to get you up and running merchandising faster than ever. These views abstract over the data flow from the App Store and display system-provided UI to represent your in-app purchases. You can even use SwiftUI APIs you're already familiar with to customize how these views integrate with your app. Just like SwiftUI, these new views are supported on all platforms, so merchandising in-app purchase will be easier than ever on iPhone, iPad, Mac, Apple Watch, and Apple TV.
A flock of feathered friends approached me and asked if I can help them add in-app purchases to their new game called Backyard Birds. With these new views from StoreKit, of course I said, "No problem at all." Join me as we deliver an excellent in-app purchase experience in Backyard Birds. Feel free to download the sample project to work through this with me. We'll use Xcode Previews to rapidly iterate on our SwiftUI views. Because we have so much to cover, I've already set up a StoreKit configuration file. This contains metadata about our in-app purchases, which is necessary to use Xcode Previews with StoreKit. We have some great sessions to help you get started in your own app, such as, "What's new in StoreKit Testing" and "Introducing StoreKit Testing in Xcode." Let's get right into Xcode. In Backyard Birds, we want to sell premium bird food like this nutrition pellet. After purchasing the food, we can leave it in our backyard to attract more hungry bird visitors. Let's jump into the code to see how we can leverage StoreKit to merchandise these products.
To get started, we'll create a view called BirdFoodShop to merchandise our bird food. I already created a file to implement this view. To use StoreKit to build our view, we need to import both StoreKit and SwiftUI at the top of the file.
Next, I'll declare a query here to get our bird food data model, which will help us build out our store.
I'm adding a StoreView to the app because it's the quickest way to get the merchandising view up and running. We need to provide it a collection of product identifiers from our StoreKit configuration file, which we can get from the birdFood model.
After this declaration, now we have a functioning merchandising view. StoreKit loads all the product identifiers from the App Store and presents them in UI for us to view. The display names, descriptions, and prices all come directly from the App Store, which uses what you've set up in App Store Connect or your StoreKit configuration file. StoreKit even handles more subtle, but important, considerations like caching the data until it expires or the system is under memory pressure and checking whether in-app purchase is disabled in Screen Time. Earlier, the bird designers sent decorative icons for each bird food product. We can add these icons to the Store View just by adding a trailing view builder, and passing in a SwiftUI view representing our icons.
The view builder takes a Product value as a parameter, which we can use to determine an icon to use. I created a helper view that takes a product ID and looks up the right icon from our asset catalog.
Once I place this in here, you can see the preview updates to show the icons for each of our products. The Store View helps us get up and running with ease by turning our product identifiers and icons into a functional and well designed store. A powerful feature of the Store View is, our products automatically adjust to different platforms, so we already have a shop that looks great on iPad, Mac, and Apple Watch. Let's change our target in Xcode to Apple Watch to preview our shop.
Looks great! I think we're ready to sell some bird food on Apple Watch as well.
It's common to want to organize your products in a way that's unique to your offering. Our team of bird designers have been hard at work creating a composition to showcase the bird food. This composition displays the best value prominently and organizes the other products into shelves. This is different from the list style layout we can achieve with StoreView, but StoreKit has us covered here as well.
For more detailed layouts, we can utilize the new ProductView. In fact, the StoreView we were just looking at uses the same ProductView to create its rows. Let's start by declaring a container for our new store.
I want to showcase this box of nutrition pellets prominently above the other products, because it's our best value. To do this, I'll declare a ProductView by providing the ID for the nutrition pellet box.
Just like with the StoreView, we can add a decorative icon by adding a trailing closure. I'll reuse our helper view from before.
Next, let's add a section below for the other food items. I'll start by placing a background behind the best value...
Then a header along with another helper view I made to lay out our bird food in shelves.
Inside this shelf helper view, we can declare a ProductView for each bird food product, along with our decorative icon.
There's one last thing we need to tie this whole shop together. We really want to display this box of nutrition pellets prominently to customers, but the bird designers think what we have now could look better. To appease the birds, we can use the new productViewStyle API to set the style for our hero product. I'll choose the large style to really make this stand out.
In just a few minutes, we built a specialized shop just for our bird food using the new ProductView in StoreKit. The large ProductView style helps us display our best value prominently by just adding one view modifier. There are three standard styles to choose from to fit your needs. Compact helps display more products in a smaller space, our bird food shelves automatically use the Regular style, and of course, the Large style is great for prominent presentations.
Since the StoreView is composed of ProductView instances, you can use the same productViewStyle modifier to change the style of the StoreView.
You can even create custom styles and use them with ProductView and StoreView. Stick around, and I'll show you how later in the session. We've built a great way to offer consumable bird food in-app purchases using ProductView. The business birds think we haven't gone far enough, and they've tasked me with offering a subscription called Backyard Birds Pass for the most avid birdwatchers. While we can build subscription UI using ProductView or StoreView, the new SubscriptionStoreView is built specifically for subscriptions. Let's return to Xcode and build this together. To get started, in our StoreKit configuration, I've created this "Backyard Birds Pass" subscription group which offers three levels of service.
Take note of this group ID. We'll need this in a moment.
Earlier, I made a new file for our pass shop, so let's dive right in to the SubscriptionStoreView. The fastest way to get up and running with the SubscriptionStoreView is by providing the group ID from our StoreKit configuration file or App Store Connect. I already added the group ID to our environment, so we can just declare an environment property to access it, and then declare a SubscriptionStoreView by providing the group ID.
Just like the StoreView and the ProductView, the SubscriptionStoreView manages the data flow for us and lays out a view with the different plan options. It also checks for existing subscriber status and whether the customer is eligible for an introductory offer. While this automatic look is great, there are some powerful new APIs we can use to make this fit the look and feel of Backyard Birds. For example, we can replace the marketing content in the header with any SwiftUI view. I built a marketing content view earlier, so I'll just drop this in here.
We can also add a container background to the Subscription Store to make things more visually interesting. We can use the new SwiftUI containerBackground API.
Notice how I'm choosing to place this in the full height of the Subscription Store and then declaring a view I created earlier with a sky gradient and some clouds. To tie everything together, we can use some other APIs for styling the Subscription Store. By default, the Subscription Store adds a material layer between the subscription controls and a full-height background.
We can use the background style modifier to make the background behind the subscription controls clear.
Now, I'll use subscriptionStoreButtonLabel to choose a multi-line layout for our subscribe button.
Notice how the subscribe button now contains both the price and "Try it Free." Next, I'll add subscriptionStorePickerItemBackground to declare a material effect for our subscription options.
Here, we can see the sky gradient shine through the subscription plan options.
Finally, because our subscription has offer codes, I'll use the new storeButton modifier to declare the Redeem Code button as visible.
With just this one view modifier, we have a button for customers to open the offer code redemption sheet.
Now, our subscription view matches the feel of the rest of Backyard Birds. While these new views significantly reduce the effort required to add in-app purchase to an app, there are a couple important pieces we're missing. First, we must add logic to actually unlock the content after a purchase is made.
Second, we need to check if someone is already subscribed and then hide any controls that present the SubscriptionStoreView. StoreKit views handle customers who are already subscribed automatically, but in many cases, the best experience is choosing not to present any merchandising UI to existing customers.
StoreKit has some brand new APIs to make implementing these important capabilities just as easy and fun as selling your content. Before you get started with these APIs, you'll want to have already implemented your business logic, or at least have some scaffolding in place. Ensure you're handling updated transactions, cooperating with your server, tracking consumable entitlements, and creating a data model that's suitable for your UI code, among other things. I recommend checking out "Meet StoreKit 2" and "What's new in App Store server APIs" to learn more about implementing your business logic.
I already went ahead and implemented our bird business logic into an actor called BirdBrain. You'll see me referencing this soon.
Let's get started with giving bird watchers access to the consumable bird food they purchase. Handling purchases that come from any of the StoreKit views is simple. You just modify a view with onInAppPurchaseCompletion and provide a function to call whenever a purchase completes. You can modify any view with this method, and it will be called whenever a descendant StoreKit view finishes a purchase. Let's add this modifier to our BirdFoodShop.
The modifier gives us the product which was purchased and the result of the purchase, whether it was successful or not. Let's implement this to send any successful results to the BirdBrain actor for processing.
By adding this modifier, we're now unlocking the consumable bird food that people purchase. Let's give this a try in the simulator.
I'll choose a backyard and tap the supplies.
Then, I'll purchase some nutrition pellets.
After the sheet dismisses, you'll notice we now have five nutrition pellets in our supplies inventory.
Now, we can place a nutrition pellet and sit back as our pellet brings all the hungry birds to the yard.
In addition to onInAppPurchaseCompletion, there are a few other related view modifiers you can use to handle events from StoreKit views.
You can use onInAppPurchaseStart to handle when someone triggers a purchase button, but before the purchase begins. This is useful if you want to update some UI components while the purchase is running, like dimming controls. The function you provide here receives the product that will be purchased as a parameter.
When using these modifiers, it's important to know that they will handle events from any descendant ProductView, StoreView, or SubscriptionStoreView instances. If you add multiple modifiers, all of your actions will run for each event. Keep in mind, using these modifiers is entirely optional. By default, successful transactions from StoreKit views will emit from the Transaction.updates sequence, but you have the option to add onInAppPurchaseCompletion to handle the result directly. You can pass nil to any of these modifiers to revert to the default behavior. Now, let's talk about handling Backyard Birds Pass subscriptions. In addition to the new view APIs, StoreKit has new view modifiers for declaring data dependencies in SwiftUI. First, I'll cover the subscriptionStatusTask, which we can use to make unlocking our pass a breeze. In any view that depends on our subscription, we can add the subscriptionStatusTask modifier. Let's start with the Backyard Grid, because this is where we show the button to open the subscription offer sheet.
The subscriptionStatusTask modifier takes the group ID of the subscription we depend on.
This is the same group ID we used earlier when we declared the SubscriptionStoreView. Now, whenever the Backyard Grid appears, a background task will load the subscription status and then call the function we provide once the task completes.
The best practice for using this API is to just pass the statuses to our business logic, in our case, the BirdBrain actor, and then have the actor process the data and return a model type that is easier to work with in our UI code.
I made this Pass status enum, so I'll just make a state property to assign this to.
Then, we can choose to only show the subscription offer card if someone is not currently subscribed.
With these quick additions, we'll now only show the offer card to bird watchers who aren't yet subscribed. StoreKit will call our function when the status changes, so our view will always reflect the most up to date information. We can use this same pattern throughout the app to unlock Backyard Birds Pass content, and we can use the onInAppPurchaseCompletion modifier to automatically dismiss the Pass Shop sheet after a successful subscription. I already completed this part earlier, so let's run the app in the iPhone simulator and give this whole thing a test.
I'll just tap Check It Out and press Try it Free.
The payment sheet appears, and I can tap Subscribe, then dismiss the alert. Watch how the offer sheet automatically dismisses, and the offer card is hidden too. That's because the subscription status task calls our function again each time the status changes, so we can be sure our app's UI is always up to date.
While we're on the topic, if your app offers non-consumables or non-renewing subscriptions, there's a new API to make checking entitlements as easy as the subscriptionStatusTask. You can use the currentEntitlementTask modifier to declare a view as dependent on the current entitlement for a product ID, and the system will asynchronously load the current entitlement and call your function with the current entitlement whenever it changes. The functions you provide to both the subscriptionStatusTask and currentEntitlementTask take an entitlement task state as a parameter. That way, you can choose to granularly handle the case when the entitlement is still loading, if it fails to load, and when the entitlement loaded successfully. I covered how these new StoreKit views help streamline in-app purchase integration in Backyard Birds. Now, I want to go a little deeper and show how you can take these views a step further with all the new StoreKit APIs for SwiftUI.
First, we'll look at more options for setting icons for ProductView and StoreView. Then, I'll go into detail about styling the Product View. After that, I'll cover how to add buttons with common functionality to the StoreView and SubscriptionStoreView. Finally, I'll go into the various new APIs you can use to make the Subscription Store View fit your brand's look and feel. Let's get into decorative icons. When you provide an icon, the standard Product View styles all show a placeholder icon while the product is loading, like what you can see on the left. Sometimes the automatic icon doesn't exactly fit what you expect the actual icon to be. For example, on iPhone, the automatic placeholder is a square, but we use circle icons for Bird Food products.
You can easily improve this appearance by adding a second trailing closure to your ProductView with the icon you want to use for your placeholder. In this case, I just provided a circle for the placeholder. If you set an App Store promotion image in App Store Connect, you can have the ProductView use that same image instead of a SwiftUI view. Just set the prefersPromotionalIcon parameter to true.
You can still provide a SwiftUI view as a fallback, but this view is ignored as long as the product has a promotional icon. Check out "What's new in StoreKit 2 and StoreKit Testing in Xcode" and "What's new in App Store Connect" to learn how to set up a promotional icon.
Even if you don't want to use a promotional icon from the App Store, you can still use the cool in-app purchase icon treatment for your icons declared in SwiftUI. Just add this modifier to the view you provide for the icon to get this border added to your view. That's all about icons in the Product View. Keep in mind, there's corresponding API to do all of the same things with Store View icons, too. Now, let's talk about styling the Product View.
Earlier in the session, I mentioned you can make custom Product View styles, and it's finally time for me to show you how.
The Product View's appearance, layout behavior, and interactions are entirely defined by the style it uses. So, if you can't find a standard style that fits what you're going for, you can always create your own custom Product View style.
The first case we'll look at involves creating custom styles composed of the standard styles so you aren't starting entirely from scratch. For example, what if you want the Product View to show a progress spinner instead of the standard placeholder appearance while loading? The first step for creating a custom style is to create a type that conforms to the ProductViewStyle protocol.
The only requirement for implementing the protocol is this makeBody method. The configuration value passed to your makeBody method has all of the properties you need to declare an excellent Product View. For example, it has a state enumeration which covers the different states of loading the product. To customize the loading appearance, we just have to declare a ProgressView for the loading state. Then, we can fallback to the standard ProductView behavior for any other states by simply passing the configuration to a ProductView instance.
You apply custom styles the same way you do standard styles, by passing it to the productViewStyle modifier. Of course, you don't need to compose your custom style with a standard style. You can always define your style using other views in the makeBody method. When the task state is success, you can access the Product value the view is representing. This is the same Product value you're already used to working with if your app uses StoreKit 2. You can use all of the properties of the Product to create your view. The configuration also allows you to access the decorative icon.
When you're adding the purchase button, make sure to use the purchase method on the configuration value, not the product value. Using the method on the configuration will add default purchase options to make sure the payment confirmation sheet displays in proximity to your Product View, and also triggers the reactive modifiers like onInAppPurchaseCompletion. Remember: when your custom style is built from scratch, the appearance and behavior of Product Views using this style will match that of the views you composed to build the style. Creating custom styles is a great way to leverage all the infrastructure for the Product View, such as App Store data flow, while being free to declare any appearance and behavior you want.
While loading, the UI we built for the Bird Food Shop shows placeholder shapes for each product. But what if we want an appearance like this loading spinner on the right? The solution to this problem is to lift up state. Let me explain what I mean here.
This diagram represents the hierarchy of the BirdFoodShop we built earlier. The BirdFoodShop has several ProductView descendants. When you initialize a ProductView with a product ID, each view internally keeps state of the product since the loading operation is asynchronous. If you want to create an effect where the parent BirdFoodShop shows a different appearance while the products are loading, you'll need to lift your state up into the parent BirdFoodShop. Once the parent BirdFoodShop is managing the state of the products, it's free to change its appearance while the data is loading, and then create ProductView instances using the pre-loaded product values instead of their IDs. We've only covered creating Product Views by product ID so far, but it's important to know you can pass a Product value that you've already loaded to a ProductView. This causes the Product View to skip loading and just lay out the merchandising view directly. You might be thinking: that's all cool, but in order to do that, now I have to write my own product request and caching logic. Well, you'll be happy to know we're exposing the insides of the StoreKit views as a view modifier, so you can declare any view as dependent on the metadata for a product ID. StoreKit will handle loading the products for you, caching them, and keeping them up to date. To do this, you just use the new storeProductsTask modifier. Similar to the subscriptionStatusTask we covered earlier, you pass a collection of product IDs for the view to depend on. Then, you get a state value you can use to handle the states of the async task. This should all feel pretty familiar after we just looked at a custom ProductViewStyle implementation.
From here, we can show our loading view on loading….
Use the new ContentUnavailableView if the products aren't available… Or just show our BirdFoodShop directly with the preloaded Product values. It's that easy. Speaking of easy, there are several useful common actions to include with in-app purchase merchandising UI. The StoreView and SubscriptionStoreView make it really easy to add auxiliary buttons for these common actions.
When I talk about auxiliary buttons, I mean buttons that perform actions which support the main purpose of the view. For example, this Cancellation button and Redeem Code button are both auxiliary to subscribing to the pass.
We already looked at adding the Redeem Code button using the storeButton modifier when we first built this sheet. Let's take a closer look at this view modifier. There are a few values you can pass for each of the two parameters here. The first parameter enables you to choose the visibility. Automatic is the default for all buttons, which causes StoreKit to choose whether to make the button visible depending on the context. You can also choose to make a button explicitly visible or hidden. The next parameter allows you to choose the button kind you want to configure the visibility for. The cancellation button shows a platform-appropriate button to dismiss the view. This button works for both the StoreView and the SubscriptionStoreView. The automatic behavior for the cancellation button is to show whenever the view is presented. On the right, the Subscription Store View is presented as a sheet so it shows the cancellation button in the top right automatically. On the left, the view isn't presented as a sheet, so there's no cancellation button. Of course, you could choose to override this behavior and hide the cancellation button when presented. Keep in mind, you only want to do this when you're replacing the cancellation button with your own cancellation button. It's good practice to always accompany your merchandising UI with a clear button to dismiss the presentation.
Just like the cancellation button, both the Store View and Subscription Store View can show a Restore Purchases button. By default, the Restore Purchases button is always hidden, but you can choose to show it in your merchandising UI with the storeButton modifier. The next three button kinds are only for the SubscriptionStoreView. We've already talked about the redeemCode button. The next button kind is a signIn button. If your subscription service allows people to subscribe outside of the App Store, it's a good idea to show a sign in button in case an existing subscriber needs to access their subscription. An important thing to know for the sign in button is, you must declare a sign in action using the new subscriptionStoreSignInAction modifier. If you set a sign in action, the sign in button will be visible automatically.
The sign in button simply calls the function you declare with subscriptionStoreSignInAction, so you can use this as a signal to run your sign in flow.
The final button kind to review is policies. You might want to show links to the terms of service and privacy policy along with your subscription offer, and the SubscriptionStoreView makes this really easy.
Typically the policy buttons are hidden by default. If you make them visible with the storeButton modifier, they'll display above the subscribe controls on iOS and Mac. Since these buttons display above your container background, the default style may not be legible against your background. Use subscriptionStorePolicyForegroundStyle to set a shape style to use for the policy buttons that is legible against your background.
Configuring auxiliary buttons with the storeButton modifier helps add powerful functionality to your merchandising UI with just a few straightforward declarations. Earlier in the session, we configured the style of the Subscription Store View to match the look and feel of Backyard Birds. Now, I want to look at these style APIs closer. First, let's look at choosing a control style. Automatically, the SubscriptionStoreView chooses a control style based on the kind of subscription you're merchandising.
You can use the new subscriptionStoreControlStyle modifier to choose the style of controls to use for your subscription plans. For example, you can choose a button per plan instead of the automatic picker.
Let's talk about the different styles of controls.
If you don't specify the style, the Subscription Store View picks a control automatically. On iPhone, this is the picker control for subscriptions with multiple plan options. You can also explicitly choose the picker control. On iOS and Mac, there's a prominent picker control, which displays the subscription plan options more prominently with a shadow and selection ring.
Last, you can choose to show a button for each subscription plan instead of the picker control. On the topic of subscribe buttons, there's a new API you can use to customize the button labels.
By default, the SubscriptionStoreView shows a subscribe button that contains an action phrase and the pricing information as a caption above the button.
You can add the subscriptionStoreButtonLabel modifier to change the button label to multiline, which causes the pricing text to be contained within the button label, instead of as a separate caption.
In addition to customizing the layout of the button label, you can also customize the content. For example, you could choose to show the display name of the selected subscription instead of an action phrase.
You can even compose a button label value with both the layout and the content by chaining the components together, like this.
Since the button controls are composed of the same subscribe buttons as the picker controls, you can use the same modifier to customize these buttons too.
For example, you can choose to only show the price in the label. This is useful when your plans are all the same service, but with different price points.
Different subscription plans use the display name and the description you set up in App Store Connect to build the controls. To make these controls more interesting, you can choose to add a decorative view for each different plan. To add the decorative view, just add the subscriptionStoreControlIcon modifier to the Subscription Store.
The modifier takes a view builder. It provides the view builder with both a Product value and a SubscriptionInfo value. Using these parameters, you can provide a different view for each plan.
These icons also work when you use the button control style for your subscription plans. Now, let's look a little closer at adding background content to the Subscription Store View. To recap from earlier, you can add a container background to the Subscription Store by modifying your marketing content with the containerBackground modifier. In this case, we're providing a gradient of our accent color for the background and choosing to place it in the Subscription Store.
You can learn more about the new containerBackground API in the session "What's new in SwiftUI." There are a few different background placements you can use for the Subscription Store. If you use the Subscription Store placement, it will choose an automatic placement based on the context. On iOS and Mac, you can explicitly specify you want your background to be placed in the Header of the Subscription Store. This placement is behind your marketing content. There's also a Full Height placement, which places the background behind the full height of the Subscription Store View.
Earlier in the session, we discussed how to use an API like the subscription status task to avoid presenting our Get Backyard Birds Pass sheet. However, there is a case when we may want to show a Subscription Store View to existing subscribers, and that's when we want to encourage subscribers to upgrade to the premium plan.
When we detect a subscriber is currently subscribed to a plan with a lower level of service than premium, we can present an upgrade sheet by passing upgrade as the visibleRelationships parameter. This could be any combination of subscription relationships we want, and it only has effects when someone is currently subscribed. Then, to make the offer more effective, we can provide a different view for the marketing content to explain the benefits of the premium plan. You can use the subscriptionStatusTask to keep track of a subscriber's level of service, and then use this information to know which offering to present to a customer. That's everything I have to cover today. When you start adding in-app purchase to your apps, declare a StoreView to get up and running quickly. If you desire a more customized layout, give ProductView a try. For your subscriptions, you can declare a SubscriptionStoreView to build compelling offers. And when you're ready to take things to the next level, try out the new view modifiers and other APIs to really make this your own. If you can't get enough StoreKit and SwiftUI, check out the sessions; "What's new in StoreKit 2 and StoreKit Testing in Xcode" and "What's new in SwiftUI" next.
Thanks for joining me today to learn about the new StoreKit APIs for SwiftUI. Happy coding!
-
-
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() } }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.