EzIAP 1.0.0

EzIAP 1.0.0

Seth Arnott 维护。



EzIAP 1.0.0

  • 作者:
  • Seth Arnott

EzIAP - 一个针对 iOS 的内购框架

一个简化 iOS 项目内购集成的框架。使用 Swift 4 编写。

已更新以与 Cocoapods 一同使用。

概述

此框架简化了在您的 iOS Swift 应用程序中设置和处理内购的过程,并由 Cocoapods 支持。

	pod 'EzIAP'

项目包括一个示例应用程序,演示如何在您的应用程序中使用该框架。您可以通过项目中的 'iap_test' 计划运行它。

项目与静态产品(您不打算更改的硬编码到您的应用程序中的产品)以及动态产品(从您的远程 API 或 json 文件加载的产品)兼容。在示例应用程序中提供了如何使用这两者的示例。请参阅下面的“使用”部分以获取示例。

请注意,测试应用程序将验证产品标识符,但产品标识符仅为占位符,因此当您运行项目时验证将失败。您需要在验证和购买/恢复测试成功之前更新或在自己的项目中使用。

有以下三种方式可以用您自己的产品标识符进行测试。

  1. 将测试项目的包标识符更改为自己的包标识符,并更新示例以使用您自己的产品标识符,将此项目作为测试床使用。
  2. 将文件复制到您自己的项目中,并更新产品标识符进行测试。
  3. 通过 Cocoapods 在您自己的项目中设置框架。

需求

  • iOS 10.0+
  • Xcode 9
  • Swift 4

用法

您可以在项目中使用此框架的两种方式。

Cocoapods

最简单的方法是使用 cocoapods。首先确保安装了 cocoapods,然后打开终端并切换到项目根目录。

然后运行

	pod init

这将在项目根目录下生成一个 Podfile 文件。打开该文件,添加 'EzIAP' 的依赖项。您的 podfile 应该看起来像这样


# Uncomment the next line to define a global platform for your project
platform :ios, '10.0'

source 'https://github.com/CocoaPods/Specs.git'

# Example target
target 'MY_APP_TARGET_NAME_HERE' do
  	use_frameworks!

    pod 'EzIAP'

end

设置好 podfile 后,在终端运行 pod install。当它完成时,关闭任何打开的 Xcode 项目,并使用新生成的 xcworkspace 打开您的项目。

手动

如果您想手动使用源文件,可以将 'EzIAP/Source/' 目录中的 'iTunesStore' 目录复制出来,并将整个目录放置到您的项目中。

-- 从现在开始,无论通过 Cocoapods 还是手动设置依赖项,设置都是相同的。 --

通过 Cocoapods 或手动设置依赖项后,您需要设置 GeneralStore 类的实例(或编写您自己的自定义版本,实现 'Store' 协议,如果您想这样做,请参见 'DemoStore' 类作为示例)。

您可以像这样设置您的 ViewModel 以使用存储器(这是 HomeViewModel 的一部分)

import EzIAP
    
	// setup store to handle purchase processing
    var store: Store!
    
    init() {
        store = setupStore()
    }

    internal func setupStore() -> GeneralStore {
        
        let store = GeneralStore()
        
        store.onTransactionInitiated = { status in
            self.showSpinner(true)
        }
        
        store.onTransactionComplete = { status in
            self.showSpinner(false)
            
            if let productID = status.transaction?.payment.productIdentifier {
                
                // Store product id in UserDefaults or some other method of tracking purchases
                UserDefaults.standard.set(true , forKey: productID)
                UserDefaults.standard.synchronize()
            }
        }
        
        store.onTransactionCancelled = { status in
            self.showSpinner(false)
        }
        
        store.onTransactionFailed = { status in
            self.showSpinner(false)
            if let error = status.error {
                print("Purchase Error: \(error.localizedDescription)")
            }
        }
        
        return store
    }

使用最简单的实现是样本应用程序中的 StaticProductsViewModel 类。它在初始化时接收一个 Store 对象。《HomeViewModel》创建 GeneralStore 的实例,并将其传递给它创建的所有 ViewModel。由于 HomeViewModel 是应用程序中的基本 ViewModel,因此所有其他 ViewModel 都使用相同的 Store。

HomeViewModel 以这种方式提供模型供子控制器使用

// MARK: View Model Vending
extension HomeViewModel {
    
    func staticViewModel() -> StaticProductsViewModel {
        return StaticProductsViewModel(store: store)
    }
    
    func dynamicViewModel() -> DynamicProductsViewModel {
        return DynamicProductsViewModel(store: store)
    }
}

以下是完整的实现

import Foundation
import EzIAP

class StaticProductsViewModel {
    
    // setup to handle purchase processing
    var store: Store
    
    // this will contain our static list of products
    internal var products: [Product] = []
    
    init(store: Store) {
        
        self.store = store
        
        // setup product list on initialization
        setupProducts()
    }
    
    func setupProducts() {

        // setup products list with static list of products 
        // we already know what all products will be so...
        products = [
            MyProduct(identifier: "com.myapp.static.id1", name: "Static Product 1", price: 1.99),
            MyProduct(identifier: "com.myapp.static.id2", name: "Static Product 2", price: 2.99),
            MyProduct(identifier: "com.myapp.static.id3", name: "Static Product 3", price: 3.99),
            MyProduct(identifier: "com.myapp.static.id4", name: "Static Product 4", price: 4.99),
            MyProduct(identifier: "com.myapp.static.id5", name: "Static Product 5", price: 5.99)
        ]
        
        // validate these product identifiers with the app store...
        store.validateProducts(products)
    }
}

extension StaticProductsViewModel: ProductSelectionDelegate {
    
    // On product selection tell the store to purchase the selected product
    func selected(product: Product) {
        store.purchaseProduct(product)
    }
}

// MARK: Data Access Functions for View Controller
extension StaticProductsViewModel {
    
    func productCount() -> Int {
        return products.count
    }
    
    func item(at index: Int) -> Product {
        return products[index]
    }
}

对于更复杂的示例,您可以在样本项目中查看 DynamicProductsViewModel 类。

它使用一个虚拟API类来“获取”产品列表。对于演示,它从一个json文件中加载数产品并返回模型对象。

以下是DynamicProductsViewModel的实现

import Foundation
import EzIAP

/// This class represents a use case where products are fetched from a remote server,
/// or where the product list may vary independent of builds submitted to the App Store.
class DynamicProductsViewModel {
    
    // setup store to handle purchase processing
    var store: Store
    
    // This is a dummy api for demo purposes
    internal var api = RemoteApi()
    
    // this will contain our products when fetched/loaded
    internal var products = [Product]()
    
    // this handler will run when the product list changes, setter should handle view updates/etc
    var onProductListUpdate: (() -> Void)?
    
    init(store: Store) {
        self.store = store
    }
    
    // fetch list of products from remote server
    func fetchProductList() {
        
        // TODO: connect to remote server...
        api.fetchProducts { [weak self] (products, error) in
            guard let products = products, error == nil else {
                return
            }
            
            // assign or append to products array as needed
            self?.products = products
            
            // this validates the products with the App Store.
            self?.store.validateProducts(products)
            
            // run closure to notify of list update
            self?.onProductListUpdate?()
        }
    }
    
    func tappedCell(indexPath: IndexPath) {
        
        // in this case tapping a cell is a purchase request... so trigger a purchase call
        store.purchaseProduct(products[indexPath.row])
    }
}

// MARK: data access functions for the view controller
extension DynamicProductsViewModel {
    
    func sectionCount() -> Int {
        return 1
    }
    
    func rowCount(section: Int) -> Int {
        return products.count
    }
    
    func item(at indexPath: IndexPath) -> Product {
        return products[indexPath.row]
    }
}