If you are unfamiliar with SwiftUI, you can read this good article.
To start we will need the latest Xcode. Create a new project and make sure you have checked "Use SwiftUI".
SwiftUI is a framework for writing user interfaces. We can’t actually create a StoreKit wrapper or something that is not a part of UI. So we need a StoreKit helper to use in our project. You can use any solution you want, for example, SwiftyStoreKit. But we will use this class from our previous article.
On our main screen we will show the list of all our subscriptions, with expiration dates for purchased ones. Let’s see how we create a main view from SceneDelegate
:
ProductsStore.shared.initializeProducts() if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: ContentView(productsStore: ProductsStore.shared)) self.window = window window.makeKeyAndVisible() }
Here we create a singleton class ProductsStore
, which initializes our products. Then a ContentView
is being created and shared instance is passed as a parameter.
class ProductsStore : ObservableObject { static let shared = ProductsStore() @Published var products: \[SKProduct\] = \[\] @Published var anyString = "123" // little trick to force reload ContentView from PurchaseView by just changing any Published value func handleUpdateStore(){ anyString = UUID().uuidString } func initializeProducts(){ IAPManager.shared.startWith(arrayOfIds: \[subscription\_1, subscription\_2\], sharedSecret: shared\_secret) { products in self.products = products } } }
Let’s dive into ProductsStore
class. It’s a small class, which has one function: loads SKProduct
objects and passes them to SwiftUI views using object observing. You can see that ProductsStore
conforms toObservableObject
.
What is ObservableObject
and @Published
keyword?
ObservableObject
is a special protocol to track changes of it's properties (marked with @Published
keyword) and reflect those changes in SwiftUI views. The only requirement of ObservableObject
protocol is to have a property with @Published
keyword which will change in some time. You can treat it like a Notification Center for SwiftUI views. In our example, when products
array changes, a notification will be sent to all views that listen for changes. To listen for changes you should add a property to your view with a special @ObservedObject
keyword.
@ObservedObject var productsStore : ProductsStore
This line of code should be added to our ContentView
in order to listen for ProductsStore
changes. ContentView
will be automatically reloaded when we received products from StoreKit.
In our example, we store products in ProductsStore
. But we already have products in IAPManager
. Although duplicating same array is not good approach, in this article I wanted to show how object observing works. Furthermore, it’s not always possible to change existing StoreKit libraries, if you use Cocoapods, for example.
It’s worth noting, that besides object observing technique, there are also a simpler @State
keyword for observing basic types (Int, String) and more global @EnvironmentObject
, which lets you to update all views at once without the need of storing a property.
Let's take a look at the ContentView
code:
struct ContentView : View { @ObservedObject var productsStore : ProductsStore @State var show\_modal = false var body: some View { VStack() { ForEach (productsStore.products, id: \\.self) { prod in Text(prod.subscriptionStatus()).lineLimit(nil).frame(height: 80) } Button(action: { print("Button Pushed") self.show\_modal = true }) { Text("Present") }.sheet(isPresented: self.$show\_modal) { PurchaseView() } } } }
Here we display subscriptions statuses for each product. Using ForEach
we create Text
views and assign a string from an extension. Since we observe productsStore
property, our view will reload each time the products array changes.
subscriptionStatus
is a simple extension function which returns a state of a subscription:
func subscriptionStatus() -> String { if let expDate = IAPManager.shared.expirationDateFor(productIdentifier) { let formatter = DateFormatter() formatter.dateStyle = .medium formatter.timeStyle = .medium let dateString = formatter.string(from: expDate) if Date() > expDate { return "Subscription expired: \\(localizedTitle) at: \\(dateString)" } else { return "Subscription active: \\(localizedTitle) until:\\(dateString)" } } else { return "Subscription not purchased: \\(localizedTitle)" } }
That’s how ContentView
looks like.
Now let’s go to purchase screen. As we know, in a purchase screen we must include a long terms text about subscription. So better to use ScrollView
here.
var body: some View { ScrollView (showsIndicators: false) { VStack { Text("Get Premium Membership").font(.title) Text("Choose one of the packages above").font(.subheadline) self.purchaseButtons() self.aboutText() self.helperButtons() self.termsText().frame(width: UIScreen.main.bounds.size.width) self.dismissButton() }.frame(width : UIScreen.main.bounds.size.width) }.disabled(self.isDisabled) }
This is the code for PurchaseView
. Inside ScrollView
you can see that two TextViews are created. Then there are a number of functions. What is happening? Well, here we separated each view to a single function. That is made for 3 reasons:
Now, let’s see some functions code:
func purchaseButtons() -> some View { // remake to ScrollView if has more than 2 products because they won't fit on screen. HStack { Spacer() ForEach(ProductsStore.shared.products, id: \\.self) { prod in PurchaseButton(block: { self.purchaseProduct(skproduct: prod) }, product: prod).disabled(IAPManager.shared.isActive(product: prod)) } Spacer() } }
Here we just create an HStack
view and using ForEach
loop add special PurchaseButton
views in it. Here’s PurchaseButton
implementation:
struct PurchaseButton : View { var block : SuccessBlock! var product : SKProduct! var body: some View { Button(action: { self.block() }) { Text(product.localizedPrice()).lineLimit(nil).multilineTextAlignment(.center).font(.subheadline) }.padding().frame(height: 50).scaledToFill().border(Color.blue, width: 1) } }
As you see, it’s a simple button which stores a completion block and calls it in an action block. And some style is being applied. The title of the button is returned from an SKProduct
’s extension function.
The payment process is implemented like this:
func purchaseProduct(skproduct : SKProduct){ print("did tap purchase product: \\(skproduct.productIdentifier)") isDisabled = true IAPManager.shared.purchaseProduct(product: skproduct, success: { self.isDisabled = false ProductsStore.shared.handleUpdateStore() self.dismiss() }) { (error) in self.isDisabled = false ProductsStore.shared.handleUpdateStore() } }
After payment successful block is called, a handleUpdateStore
method of ProductsStore
is called. This allows our root ContentView
to force reload. When PurchaseView
dismisses, root view updates their subscriptions statuses.
How dismiss modal is implemented? SwiftUI is a declarative framework, and you can’t just dismiss a view whenever you want. But there is a way of dismissing by calling a dismiss()
method of a wrapper of Environment's presentationMode
property. Here’s how we do it:
struct PurchaseView : View { @State private var isDisabled : Bool = false @Environment(\\.presentationMode) var presentationMode private func dismiss() { self.presentationMode.wrappedValue.dismiss() } func dismissButton() -> some View { Button(action: { self.dismiss() }) { Text("Not now").font(.footnote) }.padding() }
The syntax is quite complicated, but it means that we bind one of the Environment values
with our property. presentationMode
property is a part of Environment values
, a special set of global functions and properties. You may be confused, how changing a value can dismiss a modal. But this is the way SwiftUI works. You can’t perform any actions at runtime: everything is binded at the beginning.
That’s how PurchaseView
looks like.
I hope, this article will be helpful for you. Apple likes when developers use their latest technologies. If you make an app using just iOS 13 SDK and SwiftUI, there is a chance to be featured by Apple. So don’t be afraid of new technologies — use them. The full source code can be downloaded here.
To get more insights regarding subscription app revenue growth read the Apphud Blog.