大家好,我叫Vitaly,是Adapty的创始人。我们将继续讨论专门针对iOS应用程序内购买的系列文章。在上一部分中,我们介绍了创建和配置应用内购买的过程。在本文中,我们将分析最简单的付款墙(付款屏幕)的创建,以及我们在第一阶段配置的购买的初始化和处理。
创建订阅屏幕
任何使用应用程序内购买的应用程序都具有付费专区。有要求苹果定义的最小集合所需的元素和说明性文本这种屏幕。在这个阶段,我们将不会尽可能准确地执行所有操作,但是我们的版本将非常接近工作版本。

因此,我们的屏幕将包含以下功能元素:
- 标题:解释性/销售块。
- 一组用于启动购买过程的按钮。它们还将显示订阅的主要属性:以当地货币(商店的货币)显示的名称和价格。
- 恢复过去的购买按钮。使用订阅或非消耗性购买的所有应用程序都需要此元素。
Interface Builder Storyboard. ViewController, UI (UIActivityIndicatorView) , .
ViewController. , .
import StoreKit
import UIKit
class ViewController: UIViewController {
// 1:
@IBOutlet private weak var purchaseButtonA: UIButton!
@IBOutlet private weak var purchaseButtonB: UIButton!
@IBOutlet private weak var activityIndicator: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
activityIndicator.hidesWhenStopped = true
// 2:
showSpinner()
Purchases.default.initialize { [weak self] result in
guard let self = self else { return }
self.hideSpinner()
switch result {
case let .success(products):
DispatchQueue.main.async {
self.updateInterface(products: products)
}
default:
break
}
}
}
// 3:
private func updateInterface(products: [SKProduct]) {
updateButton(purchaseButtonA, with: products[0])
updateButton(purchaseButtonB, with: products[1])
}
// 4:
@IBAction func purchaseAPressed(_ sender: UIButton) { }
@IBAction func purchaseBPressed(_ sender: UIButton) { }
@IBAction func restorePressed(_ sender: UIButton) { }
}
- - UI
- viewDidLoad . , , UI, . , — . -, .
- , , , .
- - .
:
extension ViewController {
// 1:
func updateButton(_ button: UIButton, with product: SKProduct) {
let title = "\(product.title ?? product.productIdentifier) for \(product.localizedPrice)"
button.setTitle(title, for: .normal)
}
func showSpinner() {
DispatchQueue.main.async {
self.activityIndicator.startAnimating()
self.activityIndicator.isHidden = false
}
}
func hideSpinner() {
DispatchQueue.main.async {
self.activityIndicator.stopAnimating()
}
}
}Spinner
, (1) SKProduct. , extension :
extension SKProduct {
var localizedPrice: String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = priceLocale
return formatter.string(from: price)!
}
var title: String? {
switch productIdentifier {
case "barcode_month_subscription":
return "Monthly Subscription"
case "barcode_year_subscription":
return "Annual Subscription"
default:
return nil
}
}
}
Purchases
. Apple. Purchases , , SKProduct .
typealias RequestProductsResult = Result<[SKProduct], Error>
typealias PurchaseProductResult = Result<Bool, Error>
typealias RequestProductsCompletion = (RequestProductsResult) -> Void
typealias PurchaseProductCompletion = (PurchaseProductResult) -> Void
class Purchases: NSObject {
static let `default` = Purchases()
private let productIdentifiers = Set<String>(
arrayLiteral: "barcode_month_subscription", "barcode_year_subscription"
)
private var products: [String: SKProduct]?
private var productRequest: SKProductsRequest?
func initialize(completion: @escaping RequestProductsCompletion) {
requestProducts(completion: completion)
}
private var productsRequestCallbacks = [RequestProductsCompletion]()
private func requestProducts(completion: @escaping RequestProductsCompletion) {
guard productsRequestCallbacks.isEmpty else {
productsRequestCallbacks.append(completion)
return
}
productsRequestCallbacks.append(completion)
let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productRequest.delegate = self
productRequest.start()
self.productRequest = productRequest
}
}
Delegate:
extension Purchases: SKProductsRequestDelegate {
guard !response.products.isEmpty else {
print("Found 0 products")
productsRequestCallbacks.forEach { $0(.success(response.products)) }
productsRequestCallbacks.removeAll()
return
}
var products = [String: SKProduct]()
for skProduct in response.products {
print("Found product: \(skProduct.productIdentifier)")
products[skProduct.productIdentifier] = skProduct
}
self.products = products
productsRequestCallbacks.forEach { $0(.success(response.products)) }
productsRequestCallbacks.removeAll()
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Failed to load products with error:\n \(error)")
productsRequestCallbacks.forEach { $0(.failure(error)) }
productsRequestCallbacks.removeAll()
}
}
, , , enum PurchaseError, Error ( LocalizedError):
enum PurchasesError: Error {
case purchaseInProgress
case productNotFound
case unknown
}
purchaseProduct , restorePurchases — ( non-consumable ):
fileprivate var productPurchaseCallback: ((PurchaseProductResult) -> Void)?
func purchaseProduct(productId: String, completion: @escaping (PurchaseProductResult) -> Void) {
// 1:
guard productPurchaseCallback == nil else {
completion(.failure(PurchasesError.purchaseInProgress))
return
}
// 2:
guard let product = products?[productId] else {
completion(.failure(PurchasesError.productNotFound))
return
}
productPurchaseCallback = completion
// 3:
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
public func restorePurchases(completion: @escaping (PurchaseProductResult) -> Void) {
guard productPurchaseCallback == nil else {
completion(.failure(PurchasesError.purchaseInProgress))
return
}
productPurchaseCallback = completion
// 4:
SKPaymentQueue.default().restoreCompletedTransactions()
}
- , ( , , , , )
- peoductId,
- SKPaymentQueue
- , SKPaymentQueue
, , SKPaymentTransactionObserver:
extension Purchases: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
// 1:
for transaction in transactions {
switch transaction.transactionState {
// 2:
case .purchased, .restored:
if finishTransaction(transaction) {
SKPaymentQueue.default().finishTransaction(transaction)
productPurchaseCallback?(.success(true))
} else {
productPurchaseCallback?(.failure(PurchasesError.unknown))
}
// 3:
case .failed:
productPurchaseCallback?(.failure(transaction.error ?? PurchasesError.unknown))
SKPaymentQueue.default().finishTransaction(transaction)
default:
break
}
}
productPurchaseCallback = nil
}
}
extension Purchases {
// 4:
func finishTransaction(_ transaction: SKPaymentTransaction) -> Bool {
let productId = transaction.payment.productIdentifier
print("Product \(productId) successfully purchased")
return true
}
}
- ,
- , purchased restored, , , /, , finishTransaction. : consumable , , , .
- , .
- , 2: (, , UI , )
. purchasing (, ) deferred — (, ). UI.
ViewController, , , .
@IBAction func purchaseAPressed(_ sender: UIButton) {
showSpinner()
Purchases.default.purchaseProduct(productId: "barcode_month_subscription") { [weak self] _ in
self?.hideSpinner()
// Handle result
}
}
@IBAction func purchaseBPressed(_ sender: Any) {
showSpinner()
Purchases.default.purchaseProduct(productId: "barcode_year_subscription") { [weak self] _ in
self?.hideSpinner()
// Handle result
}
}
@IBAction func restorePressed(_ sender: UIButton) {
showSpinner()
Purchases.default.restorePurchases { [weak self] _ in
self?.hideSpinner()
// Handle result
}
}
, . . x401om .