The sample project used throughout this guide can be found at https://github.com/RevenueCat-Samples/storekit-views-demo-app.
At WWDC 2023 Apple introduced StoreKit views, a set of new APIs for rendering paywalls in your app using SwiftUI. These new APIs, available starting with iOS 17, enable developers to get a simple paywall up and running in their app quickly with some customization options to help them blend into your app.

In this guide, I’ll cover the three new StoreKit views, StoreView, ProductView, and SubscriptionStoreView, and show how to hook them up to your In App Purchases and integrate them into your app. I’ll also cover some of the tradeoffs of using these new APIs. To follow along and see all of this code in action, check out our StoreKit views Sample App.
NOTE: If you’re new to StoreKit, I highly recommend reading our StoreKit 2 tutorial. It will walk you through setting up App Store Connect, StoreKit configuration files, and how to handle purchase events for unlocking your in-app features.
For all of the examples in this guide I am assuming these products are set up in App Store Connect (for our sample project these products are set up in a local StoreKitConfiguration file).
Auto-Renewable Subscription
- Group: Pro
- Products:
- “pro_weekly”
- $0.49 / week
- “pro_monthly”
- $0.99 / month
- “pro_yearly”
- $12.99 / year
- Includes 1 week free trial
- “pro_weekly”
- Products:
In-App Purchases
- “pro_lifetime” – $89.99
How to use StoreView
Let’s start with StoreView. This is the most straightforward of the new StoreKit views. Here’s a simple code example.
1let productIds = ["pro_weekly", "pro_monthly", "pro_yearly", "pro_lifetime"]
2StoreView(ids: productIds)
With just this code, you can render a paywall showing each of your in-app purchases that looks like this.

This renders each of our auto-renewing subscription products as well as our one in-app purchase product in a simple list with a button for each that initiates a purchase.
If you have already made a purchase, StoreView will automatically show that product as purchased and disable the button. If you are subscribed to an auto-renewing plan and tap on a different plan within that subscription group, it will allow you to upgrade or downgrade and iOS’s built-in in-app purchase flow will explain how that billing works.
For a lifetime in-app purchase option outside of the subscription group however, the StoreView will not automatically cancel a user’s subscription if they purchase your lifetime option. So if you want to support that you’ll have to explain to your users to do that manually.
Out of the box this is pretty boring, so let’s spruce this up by adding an image for each product. You simply return a SwiftUI view in the StoreView’s callback and it will render it for each product. I’ve created a simple SwiftUI view called ProductImage that takes in a product id and renders a unique image for each that I’ll use here.
1let productIds = ["pro_weekly", "pro_monthly", "pro_yearly", "pro_lifetime"]
2
3StoreView(ids: productIds) { product in
4 ProductImage(productId: product.id)
5}
Which renders this:

You can also add a button for users to restore their purchase using the storeButton modifier.
1let productIds = ["pro_weekly", "pro_monthly", "pro_yearly", "pro_lifetime"]
2
3StoreView(ids: productIds) { product in
4 ProductImage(productId: product.id)
5}
6.storeButton(.visible, for: .restorePurchases)
Which renders like this:

Each of these products look a little chunky, so one thing we can do is set the productViewStyle to compact. There are 3 different productViewStyles you can set, compact, regular, and large.
1let productIds = ["pro_weekly", "pro_monthly", "pro_yearly", "pro_lifetime"]
2
3StoreView(ids: productIds) { product in
4 ProductImage(productId: product.id)
5}
6.productViewStyle(.compact)
7.storeButton(.visible, for: .restorePurchases)

In the current Xcode beta as of writing (beta 6), there doesn’t appear to be a way to disable the close button on the top right corner. According to Apple’s documentation you should be able to hide this by adding a storeButton(.hidden, for: .close) modifier to your StoreView, but that API does not appear to be working in the current beta.
As far as other customizations, you can use the background and foregroundColor modifiers to tweak the look a little bit, but things are fairly limited. For a little bit more control, let’s check out ProductView.
How to use ProductView
ProductView is a SwiftUI view that renders a single StoreKit product. It’s basically the building block that StoreView is using to show your whole paywall. Using ProductView you can make a paywall that’s much more customized to fit your app.
Each ProductView takes a single productId and renders a product just like you saw in the StoreView. And just like the StoreView, you can customize the style into 3 different types: compact, regular, and large. You can also add an image by passing a SwiftUI view into the ProductView callback.
1ProductView(id: "pro_monthly") {
2 ProductImage(productId: "pro_monthly")
3}
4.productViewStyle(.compact)
This renders a functional, though pretty boring, view for purchasing an individual product.

To spruce things up you can add a background with rounded corners, some padding, and tweak the foreground color like this.
1ProductView(id: “pro_monthly”) {
2 ProductImage(productId: “pro_monthly”)
3}
4.padding()
5.background(.thinMaterial, in: .rect(cornerRadius: 20))
6.productViewStyle(.compact)
7.foregroundColor(.black)
8.padding(.horizontal)
Now that you’re controlling each element separately you can mix and match different productViewStyle’s within a custom SwiftUI view to build a much more custom paywall.

There are a few drawbacks to this approach. ProductView will not handle rendering the Restore Purchases button for you like StoreView will, so you’ll have to render that on your own. You can refer to our StoreKit 2 guide or look at this guide’s sample project for an example of how to do that.
ProductView also behaves the same way as StoreView when it comes to mixing auto-renewing subscriptions with one time in-app purchases. Upgrades and downgrades between auto-renewing subscriptions will work automatically, but users will be able to purchase a lifetime in-app purchase and the system will not automatically cancel their subscription.
How to use SubscriptionStoreView
This brings us to the last new StoreKit view, SubscriptionStoreView. This SwiftUI view is specifically tailored for rendering a paywall for a single subscription group set up in App Store Connect. This means in our example, it will not render our lifetime in-app purchase product at all. If you want to build a paywall that mixes auto-renewing subscriptions and in-app purchases, this is not the view for you.
If you are sticking with just a single auto-renewing subscription group however, SubscriptionStoreView is more powerful than StoreView or ProductView.
Let’s start with the most basic implementation. You can initialize a SubscriptionStoreView with a subscription group id, or a list of product ids if you don’t want to include all of the products in a group for some reason.
1SubscriptionStoreView(groupID: "GROUP-ID")
This one line of code will render this view.

There’s actually a lot going on here. Starting from the top, it renders your app icon with the name of your subscription group, in this case the excitingly named “Pro”.
Next it shows each of the products in your subscription group as selectable elements. The yearly product picks up that there is a 1 week trial and renders that as well.
At the bottom we have a clear CTA button that updates based on the user’s selection. If the user is eligible for a trial for the selected product, it shows “Try It Free”. Otherwise it shows “Subscribe”. The text above the button also updates based on the selection with the appropriate description for how billing works for the selected plan. This is an area that’s frequently rejected by App Review for stylistic reasons so presumably the choices Apple is making here should make it less likely to get a rejection.
If the user has already subscribed to a product within the group, SubscriptionStoreView gives it a clear badge and still allows the user to upgrade or downgrade their plan.

If you want to change the copy of the CTA button you can choose between a few options using the subscriptionStoreButtonLabel modifier. The current options are action, displayName, price, multiline, and singleline. Apple is clear to point out that these options are preferences and are not guaranteed to behave the same way on all platforms.

Similar to StoreView, you can add a “Restore Purchases” button at the bottom with the storeButton modifier. You can also add a “Redeem Code” button this way on the SubsctiptionStoreView. Additionally, you can add links to your Privacy Policy and Terms of use using the subscriptionStorePolicyDestination modifier.
1SubscriptionStoreView(groupID: PurchaseManager.subscriptionGroupId)
2 .subscriptionStoreButtonLabel(.multiline)
3 .storeButton(.visible, for: .restorePurchases)
4 .storeButton(.visible, for: .redeemCode)
5 .subscriptionStorePolicyDestination(url: privacyUrl, for: .privacyPolicy)
6 .subscriptionStorePolicyDestination(url: termsUrl, for: .termsOfService)

One final functional element before getting into styling is visibleRelationships. When initializing your SubscriptionStoreView you can set the visibleRelationships parameter to crossgrade, upgrade, or all. This will change which products are presented to the user if they are already subscribed to another product in your subscription group. For more information on subscription groups you can read iOS Subscription Groups Explained.
Ok, so you have a subscription paywall that’s functionally complete, but… well it looks pretty bland. Let’s look at the ways we can customize its appearance.
Just like StoreView and ProductView, You can add an image for each product using the subscriptionStoreControlIcon. It renders a bit smaller though so you may want to lean more towards a smaller icon as the modifier name suggests.
1SubscriptionStoreView(groupID: PurchaseManager.subscriptionGroupId)
2 .subscriptionStoreControlIcon { product, subscriptionInfo in
3 ProductImage(productId: product.id)
4 .frame(height: 18)
5 }

The marketing content at the top looks pretty rough. Fortunately we can pass in a SwiftUI view to make this look however we want. We do this by passing a view into the SubscriptionStoreView’s callback.
1SubscriptionStoreView(groupID: PurchaseManager.subscriptionGroupId) {
2 VStack(spacing: 16) {
3 Image(systemName: "dollarsign.square.fill")
4 .resizable()
5 .scaledToFit()
6 .foregroundColor(.green)
7 .frame(height: 100)
8
9 Text("Subscribe to Pro!")
10 .font(.title)
11 .bold()
12
13 Text("Please buy me 🙏")
14 .font(.subheadline.weight(.medium))
15 .fontDesign(.rounded)
16 .multilineTextAlignment(.center)
17 .foregroundColor(.gray)
18 }
19 .padding()
20}

To add some color, let’s use the containerBackground(for:) modifier on the marketing content SwiftUI view to add a gradient background. If you set the placement to subscriptionStoreHeader the background will only render behind your marketing content. If you use subscriptionStoreFullHeight it will render your background behind the full screen.

1SubscriptionStoreView(groupID: PurchaseManager.subscriptionGroupId) {
2 VStack(spacing: 16) {
3 Image(systemName: "dollarsign.square.fill")
4 .resizable()
5 .scaledToFit()
6 .foregroundColor(.green)
7 .frame(height: 100)
8
9 Text("Subscribe to Pro!")
10 .font(.title)
11 .bold()
12
13 Text("Please buy me 🙏")
14 .font(.subheadline.weight(.medium))
15 .fontDesign(.rounded)
16 .multilineTextAlignment(.center)
17 }
18 .padding()
19 .foregroundColor(.white)
20 .containerBackground(for: .subscriptionStoreFullHeight) {
21 LinearGradient(colors: [.blue, .red], startPoint: .topLeading, endPoint: .bottomTrailing)
22 }
23
24}
By default, SubscriptionStoreView renders a material background behind all of the products. You can hide this using the backgroundStyle view modifier set to clear. You can then tweak the background behind each individual product using the subscriptionStorePickerItemBackground.
1SubscriptionStoreView(groupID: PurchaseManager.subscriptionGroupId) {
2 VStack(spacing: 16) {
3 Image(systemName: "dollarsign.square.fill")
4 .resizable()
5 .scaledToFit()
6 .foregroundColor(.green)
7 .frame(height: 100)
8
9 Text("Subscribe to Pro!")
10 .font(.title)
11 .bold()
12
13 Text("Please buy me 🙏")
14 .font(.subheadline.weight(.medium))
15 .fontDesign(.rounded)
16 .multilineTextAlignment(.center)
17 }
18 .padding()
19 .foregroundColor(.white)
20
