📜 ⬆️ ⬇️

SwiftUI and auto-renewable subscriptions

image


Hello! In touch, Denis from Apphud is a service for analyzing renewable subscriptions for iOS applications.


As you know, at WWDC 2019, Apple announced their new declarative framework, SwiftUI. In this article I will try to tell you how to make payment screens using SwiftUI and implement the functionality of auto-renewable subscriptions.


If you are not familiar with SwiftUI, you can read a short introductory article . And if you want to learn more about subscriptions and how to implement them correctly, then read this article .

You will need Xcode 11 to work . Create a new project and make sure that there is a check mark next to “Use SwiftUI”.


SwiftUI is a framework for writing an interface, and therefore we cannot create a purchase manager using it. But we will not write our manager, but use a ready-made solution that we supplement with our code. You can use, for example, SwiftyStoreKit . In our example, we will use the class from our previous article .


Products will be initialized on the main screen, the date of expiry of our subscriptions and a button to go to the purchase screen will also be displayed there.


ProductsStore.shared.initializeProducts() let window = UIWindow(frame: UIScreen.main.bounds) window.rootViewController = UIHostingController(rootView: ContentView(productsStore: ProductsStore.shared)) self.window = window window.makeKeyAndVisible() 

Consider the SceneDelegate class. In it, we create a singleton class ProductsStore , in which the products are initialized. After that we create our root ContentView and specify singleton as an input parameter:


 class ProductsStore : BindableObject { static let shared = ProductsStore() var products: [SKProduct] = [] { didSet { handleUpdateStore() } } func initializeProducts(){ IAPManager.shared.startWith(arrayOfIds: [subscription_1, subscription_2], sharedSecret: shared_secret) { products in self.products = products } } } 

Consider the ProductsStore class. This is a small class, a kind of “add-on” above IAPManager , that serves to update ContentView when updating the product list. The ProductsStore class supports the BindableObject protocol.


What are BindableObject and @ObjectBinding ?


BindableObject is a special protocol for binding (binding) objects and tracking their changes. The only condition of the protocol is the presence of the didChange variable that directly sends the notification. In the example, a notification is sent when the Products array is modified, but you can add this notification for any methods and properties of the object.


The product download itself can be done in any way, but when completing this request, you must assign an array of products to the products variable. The didChange.send() function didChange.send() send a notification.


 var didChange = PassthroughSubject<Void, Never>() var products: [SKProduct] = [] { didSet { didChange.send() } } 

Simply put, it is something like Notification Center. And for your View receive these notifications, you must have a variable of this object with the @ObjectBinding attribute.


Let's return to the logic of the ProductsStore class. Its main purpose is to download and store a list of products. But the array of products is already stored in IAPManager , duplication occurs. This is not good, but, firstly, in this article I wanted to show you how the binding of objects is implemented, and, secondly, it is not always possible to change the ready-made class of the purchase manager. For example, if you use third-party libraries, you cannot add the BindableObject protocol and send notifications.


It is worth noting that in addition to the @ObjectBinding attribute @ObjectBinding there is also an @State attribute that helps to track changes in simple variables (for example, String or Int ) and a more global @EnvironmentObject , which can update all of the View in the application without having to pass the variable between objects.


Let's ContentView to the start screen ContentView :


 struct ContentView : View { @ObjectBinding var productsStore : ProductsStore var body: some View { VStack() { ForEach (productsStore.products.identified(by: \.self)) { prod in Text(prod.subscriptionStatus()).lineLimit(nil).frame(height: 80) } PresentationButton(destination: PurchaseView(), label: { Text("Present") }) } } } 

Let's deal with the code. With ForEach we create a text View , the number of which is equal to the number of products. Since we have bind the productsStore variable, the View will be updated whenever the array of products in the ProductsStore class changes.


The subscriptionStatus method is included in the SKProduct class SKProduct and returns the desired text depending on the subscription expiration date:


 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)" } } 

This is our start screen.
This is our start screen.

We now turn to the subscription screen. Since, according to the rules of Apple, the payment screen must have a long text of the conditions of purchase, it is reasonable to use ScrollView .


 var body: some View { ScrollView (alwaysBounceVertical: true, showsVerticalIndicator: 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) } 

In this example, we created two text views with different fonts. Then all the other views are highlighted in their own methods. This is done for three reasons:


  1. The code becomes more readable and understandable to learn.


  2. At the time of writing, Xcode 11 Beta often freezes and cannot compile code, and putting parts of code by function helps the compiler.


  3. Show that the view can be carried out in separate functions, facilitating the body .



Consider the purchaseButtons() method:


 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.identified(by: \.self)) { prod in PurchaseButton(block: { self.purchaseProduct(skproduct: prod) }, product: prod).disabled(IAPManager.shared.isActive(product: prod)) } Spacer() } } 

Here we create a horizontal stack and in the ForEach loop we create a custom PurchaseButton , into which we transfer the product and the callback block.


PurchaseButton class:


 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).border(Color.blue, width: 1, cornerRadius: 10).scaledToFill() } } 

This is the usual button that stores and calls the block passed when the object is created. A rounding stroke is applied to it. The text displays the price of the product and the length of the subscription period in the localizedPrice() method.


Subscription purchase is implemented as follows:


 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() } } 

As you can see, after the purchase is handleUpdateStore , the handleUpdateStore method is handleUpdateStore , with which a notification is sent to update ContentView . This is done to ensure that ContentView status of subscriptions when hiding a modal screen. The dismiss method hides a modal window.


Since SwiftUI is a declarative framework, modal window hiding is not implemented as usual. We only change the isPresented variable by declaring it with the @Environment attribute:


 struct PurchaseView : View { @State private var isDisabled : Bool = false @Environment(\.isPresented) var isPresented: Binding<Bool>? private func dismiss() { self.isPresented?.value = false } ... 

The variable isPresented is part of Environment Values - special sets of global methods and properties. You might be surprised how a variable change can hide a modal window. However, in SwiftUI, almost all actions occur when the values ​​of variables change, it is impossible to do something at runtime in the literal sense of the word - everything is fixed in advance.


Subscription Purchase Screen
Subscription Purchase Screen

Conclusion


I hope this article will be useful to you. Apple loves it when developers use its latest technology. If you release an iOS 13 application using SwiftUI, there is a potential likelihood of being an Apple branded product. So do not be afraid of new technologies - use them. You can download the full project code here .


What to read?



')

Source: https://habr.com/ru/post/458116/


All Articles