مرحباً بالجميع ، اسمي فيتالي ، أنا مؤسس Adapty. نواصل سلسلة المقالات المخصصة لعمليات الشراء داخل التطبيق في تطبيقات iOS. في الجزء السابق ، تناولنا عملية إنشاء عمليات الشراء داخل التطبيق وتكوينها. في هذه المقالة ، سنقوم بتحليل إنشاء أبسط نظام حظر الاشتراك غير المدفوع (شاشة الدفع) ، بالإضافة إلى تهيئة ومعالجة عمليات الشراء التي قمنا بتكوينها في المرحلة الأولى .
قم بإنشاء شاشة اشتراك
يحتوي أي تطبيق يستخدم عمليات شراء داخل التطبيق على نظام حظر الاشتراك غير المدفوع. هناك متطلبات من Apple تحدد الحد الأدنى من العناصر المطلوبة والنصوص التوضيحية لهذه الشاشات. في هذه المرحلة ، لن نقوم بتنفيذها جميعًا بأكبر قدر ممكن من الدقة ، ولكن نسختنا ستكون قريبة جدًا من إصدار العمل.
لذلك ، ستتألف شاشتنا من العناصر الوظيفية التالية:
- العنوان: توضيحي / كتل بيع.
- مجموعة من الأزرار لبدء عملية الشراء. ستعرض أيضًا الخصائص الرئيسية للاشتراكات: الاسم والسعر بالعملة المحلية (عملة المتجر).
- زر استعادة المشتريات السابقة. هذا العنصر مطلوب لجميع التطبيقات التي تستخدم الاشتراكات أو عمليات الشراء غير الاستهلاكية.
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 .