- PVSM.RU - https://www.pvsm.ru -

Привет! На связи Денис из Apphud – сервиса по аналитике возобновляемых подписок для iOS-приложений.
Как вы знаете, на WWDC 2019 Apple анонсировали свой новый декларативный фреймворк SwiftUI. В этой статье я попробую рассказать как с помощью SwiftUI сделать экраны оплаты и реализовать функционал авто-возобновляемых подписок.
Если вы еще не знакомы со SwiftUI, то можете прочитать небольшую вводную статью [1]. А если вы хотите побольше узнать о подписках и как их правильно реализовать, то прочитайте эту статью [2].
Для работы вам понадобится Xcode 11 [3]. Создайте новый проект и убедитесь, что стоит галочка рядом с “Use SwiftUI”.
SwiftUI – фреймворк для написания интерфейса, и поэтому мы не можем с помощью него создать менеджер покупок. Но мы и не будем писать свой менеджер, а используем готовое решение, которое дополним своим кодом. Вы можете использовать, например, SwiftyStoreKit [4]. В нашем примере мы будем использовать класс [5] из нашей предыдущей статьи [2].
Инициализация продуктов будет происходить на главном экране, там же будет отображаться дата истечения наших подписок и кнопка перехода на экран покупки.
ProductsStore.shared.initializeProducts()
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIHostingController(rootView: ContentView(productsStore: ProductsStore.shared))
self.window = window
window.makeKeyAndVisible()
Рассмотрим класс SceneDelegate. В нем мы создаем singleton-класс ProductsStore, в котором происходит инициализация продуктов. После этого создаем наш рутовый ContentView и указываем singleton в качестве входного параметра:
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
}
}
}
Рассмотрим класс ProductsStore. Это небольшой класс, эдакая “надстройка” над IAPManager, служит, чтобы обновлять ContentView при обновлении списка продуктов. Класс ProductsStore поддерживает протокол BindableObject.
Что такое BindableObject и @ObjectBinding?
BindableObject – это особый протокол для биндинга (binding) объектов и отслеживания их изменений. Единственным условием протокола является наличие переменной didChange, непосредственно отправляющей уведомления. В примере уведомление отправляется при изменении массива Products, но вы можете добавить это уведомление для любых методов и свойств объекта.
Сама загрузка продуктов может осуществляться любым способом, но при завершении данного запроса вы должны присвоить массив продуктов переменной products. Функция didChange.send() отправит уведомление.
var didChange = PassthroughSubject<Void, Never>()
var products: [SKProduct] = [] {
didSet {
didChange.send()
}
}
Проще говоря, это нечто похожее на Notification Center. А чтобы ваши View принимали эти уведомления, вы должны иметь переменную данного объекта с атрибутом @ObjectBinding.
Вернемся к логике класса ProductsStore. Его основное назначение – это загружать и хранить список продуктов. Но массив продуктов уже хранится в IAPManager, происходит дублирование. Это нехорошо, но, во-первых, в данной статье я хотел показать вам, как реализован биндинг объектов, а, во-вторых, не всегда получается изменять готовый класс менеджера покупок. Например, если вы используете сторонние библиотеки, то не сможете добавить протокол BindableObject и отправлять уведомления.
Стоит отметить, что кроме атрибута @ObjectBinding есть еще и атрибут @State, помогающий отслеживать изменение простых переменных (например, String или Int) и более глобальный @EnvironmentObject, который может обновлять сразу все View в приложении без необходимости передавать переменную между объектами.
Перейдем к стартовому экрану 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")
})
}
}
}
Давайте разберемся с кодом. С помощью ForEach мы создаем текстовые View, количество которых равно количеству продуктов. Так как мы забиндили переменную productsStore, то View будет обновляться всякий раз, когда изменится массив продуктов в классе ProductsStore.
Метод subscriptionStatus входит в расширение класса SKProduct и возвращает нужный текст в зависимости от даты истечения подписки:
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 (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)
}
В это примере мы создали две текстовые вью с разным шрифтом. Далее все остальные вью выделены в собственные методы. Это сделано по трем причинам:
Код становится более читабельным и понятным для изучения.
На момент написания статьи Xcode 11 Beta часто зависает и не может скомпилировать код, а вынесение частей кода по функциям помогает компилятору.
Показать, что вью можно выносить в отдельные функции, облегчая 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.identified(by: .self)) { prod in
PurchaseButton(block: {
self.purchaseProduct(skproduct: prod)
}, product: prod).disabled(IAPManager.shared.isActive(product: prod))
}
Spacer()
}
}
Здесь мы создаем горизонтальный стек и в цикле ForEach создаем кастомный PurchaseButton, в который передаем продукт и callback-блок.
Класс 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).border(Color.blue, width: 1, cornerRadius: 10).scaledToFill()
}
}
Это обычная кнопка, которая хранит и вызывает блок переданный при создании объекта. К нему применяется обводка с закруглением. В качестве текста отображаем цену продукта и длительность периода подписки в методе 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, с помощью которого отправляется уведомление на обновление ContentView. Это сделано для того, чтобы в ContentView обновился статус подписок при скрытии модального экрана. Метод dismiss скрывает модальное окно.
Так как SwiftUI – декларативный фреймворк, то скрытие модального окна реализуется не так, как обычно. Мы лишь изменяем переменную isPresented, объявляя ее с атрибутом @Environment:
struct PurchaseView : View {
@State private var isDisabled : Bool = false
@Environment(.isPresented) var isPresented: Binding<Bool>?
private func dismiss() {
self.isPresented?.value = false
}
...
Переменная isPresented является частью Environment Values – специальных наборов глобальных методов и свойств. Вас может удивить, как изменение переменной может скрывать модальное окно. Однако в SwiftUI почти все действия происходят при изменении значений переменных, сделать что-либо в runtime в прямом смысле слова нельзя — все забиндено заранее.
Экран покупки подписок
Надеюсь, данная статья будет вам полезна. Apple любит, когда разработчики используют ее новейшие технологии. Если вы выпустите приложение под iOS 13 с использованием SwiftUI, есть потенциальная вероятность быть зафичеринным Apple. Так что не бойтесь новых технологий – используйте их. Полный код проекта вы можете скачать здесь [6].
Автор: miden16
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios-development/322276
Ссылки в тексте:
[1] вводную статью: https://habr.com/ru/post/455970/
[2] эту статью: https://habr.com/ru/post/456602/
[3] Xcode 11: https://developer.apple.com/download/
[4] SwiftyStoreKit: https://github.com/bizz84/SwiftyStoreKit
[5] класс: https://gist.github.com/ren6/2e04b222c736df18cc7919c5e6116c26
[6] здесь: https://github.com/apphud/ios-swiftui-subscriptions
[7] Руководство по Apple Subscriptions Notifications для iOS. Так ли они хороши на самом деле?: https://habr.com/ru/post/453770/
[8] Источник: https://habr.com/ru/post/458116/?utm_campaign=458116&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.