SwiftUI和自动续订订阅

图片


你好 Apphud的Connected Denis-一种服务,用于分析iOS应用程序的可再生订阅。


如您所知,Apple在WWDC 2019上宣布了其新的声明性SwiftUI框架。 在本文中,我将尝试说明如何使用SwiftUI进行付款屏幕和实现自动续订的功能。


如果您还不熟悉SwiftUI,则可以阅读简短的介绍性文章 。 并且,如果您想了解有关订阅以及如何正确实现订阅的更多信息,请阅读本文

您需要Xcode 11才能工作 。 创建一个新项目,并确保“ Use SwiftUI”旁边有一个复选标记


SwiftUI是用于编写接口的框架,因此我们无法使用该接口创建采购经理。 但是,我们不会写经理,而是使用现成的解决方案,并在其中补充代码。 您可以使用例如SwiftyStoreKit 。 在我们的示例中,我们将使用上一篇文章中


产品将在主屏幕上初始化,我们订阅的到期日期和切换到购买屏幕的按钮也将在那里显示。


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

考虑SceneDelegate类。 在其中,我们创建一个单例类ProductsStore ,在其中初始化产品。 之后,创建我们的根ContentView并指定singleton作为输入参数。
考虑ProductsStore类:


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

这个小类,是IAPManager上的“附加组件”,用于在更新产品列表时更新ContentViewProductsStore类支持ObservableObject协议。


什么是ObservableObject@Published


ObservableObject是用于观察对象并跟踪其属性变化的特殊协议。 必须使用@Published属性标记属性。 在该示例中,当products数组更改时会发送通知,但是您可以为对象的任何方法和属性添加此通知。


产品加载本身可以通过任何方式完成,但是在此请求结束时,您必须将产品数组分配给products变量。 如何聆听变化? 这是使用@ObservedObject键参数完成的:


 @ObservedObject var productsStore : ProductsStore 

简而言之,这类似于通知中心。 为了使View接受这些通知,您必须具有@ObservedObject属性的此对象的变量。


让我们回到ProductsStore类的逻辑。 其主要目的是下载和存储产品列表。 但是产品数组已经存储在IAPManager ,发生重复。 这不好,但是,首先,在本文中,我想向您展示如何实现对象合并,其次,并非总是可以更改采购经理的完成的类。 例如,如果您使用第三方库,则将无法添加ObservableObject协议并发送通知。


值得注意的是,除了@ObservedObject属性之外@ObservedObject还有@State属性可以帮助跟踪简单变量(例如StringInt )中的更改以及更具全局性的@EnvironmentObject ,后者可以立即更新应用程序中的所有View而不必在对象之间传递变量。


让我们ContentView开始屏幕:


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

让我们找出代码。 使用ForEach我们创建文本View ,其数量等于产品的数量。 由于我们绑定了变量productsStore ,因此只要ProductsStore类中的ProductsStore数组发生更改,就将更新View


subscriptionStatus方法是SKProductSKProduct一部分,并根据预订的到期日期返回所需的文本:


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

这是我们的开始屏幕
这是我们的开始屏幕


现在转到订阅屏幕。 根据Apple的规定,由于付款屏幕上应包含购买条件的长文字,因此使用ScrollView是明智的。


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

在此示例中,我们创建了两个具有不同字体的文本视图。 此外,所有其他视图均以其自己的方法突出显示。 这样做有以下三个原因:


  1. 该代码变得更具可读性和可读性,可供研究。


  2. 在撰写本文时,Xcode 11 Beta通常会冻结并且无法编译代码,将部分代码放入函数中有助于编译器。


  3. 表明可以将视图分为不同的功能,从而使body更轻松。



考虑purchaseButtons()方法:


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

在这里,我们创建了一个水平堆栈,并在ForEach循环中创建了一个自定义PurchaseButton ,将产品和回调块传递到其中。


PurchaseButton类:


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

这是一个普通按钮,用于存储和调用创建对象时传递的块。 对其应用了舍入笔触。 我们在localizedPrice()方法中以文本形式显示产品的价格和订阅期的持续时间。


订阅购买的实现方式如下:


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

如您所见,购买handleUpdateStore ,将handleUpdateStore方法,并在该方法的帮助下发送通知以更新ContentView 。 这是为了确保在隐藏模式屏幕时ContentView订阅ContentView状态。 dismiss方法隐藏模式窗口。


由于SwiftUI是声明性框架,因此无法像往常一样实现隐藏模式窗口。 我们必须在presentationMode变量的包装器上调用dismiss()方法,并使用@Environment属性进行声明:


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

presentationMode变量是“ 环境值”的一部分-特殊的全局方法和属性集。 在SwiftUI中,几乎所有动作都是在更改变量的值时发生的,您无法在运行时按字面意义做任何事情-一切都是预先限定的。 为了在运行时执行某些操作,您需要使用包装器。


订阅购买画面
订阅购买画面


结论


希望本文对您有所帮助。 当开发人员使用其最新技术时,Apple会喜欢。 如果您使用SwiftUI发布适用于iOS 13的应用程序,则很有可能成为臭名昭著的Apple。 因此,不要害怕新技术-使用它们。 您可以在此处下载完整的项目代码。


是否想在10分钟内在iOS应用中实现订阅? 整合Apphud并:
  • 仅使用一种方法进行购买;
  • 自动跟踪每个用户的订阅状态;
  • 轻松整合订阅优惠
  • 将订阅事件发送到Amplitude,Mixpanel,Slack和Telegram,并考虑到用户的本地货币;
  • 降低应用程序的客户流失率并返回未订阅的用户。


读什么?


Source: https://habr.com/ru/post/zh-CN458116/


All Articles