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.
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:
The code becomes more readable and understandable to learn.
At the time of writing, Xcode 11 Beta often freezes and cannot compile code, and putting parts of code by function helps the compiler.
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
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 .
Source: https://habr.com/ru/post/458116/
All Articles