iOS应用内购买(IAP)指南 - StoreKit 2详解
iOS内购产品类型详解
iOS内购(IAP)提供四种产品类型,每种类型适用于不同场景:
- Name
消耗型(Consumable)
- Type
- concept
- Description
- 购买后会立即消耗或可重复购买
- 用户用完后无法恢复
- 适用场景:游戏虚拟货币、付费道具、一次性解锁内容
- Name
非消耗型(Non-Consumable)
- Type
- concept
- Description
- 购买后永久拥有,不会消失
- 可通过Apple ID恢复购买
- 适用场景:解锁高级功能、购买特定内容、永久会员权益
- Name
自动续订订阅(Auto-Renewable)
- Type
- concept
- Description
- 按固定周期(周/月/年)自动扣费
- 用户可随时取消续订
- 适用场景:流媒体服务、云存储、持续更新内容
- Name
非续订订阅(Non-Renewing)
- Type
- concept
- Description
- 需手动续费,不自动扣款
- 提供有时间限制的内容访问权
- 适用场景:限时会员、课程、期刊订阅
产品类型对比
产品类型 可重复购买 可恢复购买 适用场景
消耗型 ✅ ❌ 游戏道具、虚拟货币
非消耗型 ❌ ✅ 解锁功能、永久权益
自动续订订阅 ❌ ✅ 持续服务、定期更新
非续订订阅 ✅ ❌ 课程、赛事、短期会员
什么是应用内购买?
应用内购买(In-App Purchase, IAP)是苹果提供的允许开发者在应用内销售数字内容和服务的机制。
- Name
购买类型
- Type
- concept
- Description
- 消耗型项目(如游戏币)
- 非消耗型项目(如解锁功能)
- 自动续期订阅
- 非续期订阅
- Name
主要功能
- Type
- meaning
- Description
- 内容销售
- 功能解锁
- 订阅服务
- 安全交易
准备工作
// 1. App Store Connect配置
- 创建应用内购买项目
- 设置价格和描述
- 提交审核
// 2. 项目配置
- 添加StoreKit框架
- 配置App ID
- 设置测试账号
StoreKit 2内购管理类详解
StoreKit 2是苹果在iOS 15中引入的新API,提供了更简洁、更安全的内购实现方式。本教程将详细解析一个完整的Store
类实现。
- Name
核心特性
- Type
- concept
- Description
- 异步/等待API
- 内置收据验证
- 交易自动更新
- 订阅状态管理
- 家庭共享支持
基本类型定义
import Foundation
import StoreKit
typealias Transaction = StoreKit.Transaction
typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo
typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState
public enum StoreError: Error {
case failedVerification
}
// 定义应用的订阅权限等级,按服务级别排序,最高级别的服务在前
// 数值级别与StoreKit配置文件或App Store Connect中配置的订阅级别匹配
public enum ServiceEntitlement: Int, Comparable {
case notEntitled = 0
case pro = 1
case premium = 2
case standard = 3
init?(for product: Product) {
// 产品必须是订阅才能有服务权限
guard let subscription = product.subscription else {
return nil
}
if #available(iOS 16.4, *) {
self.init(rawValue: subscription.groupLevel)
} else {
switch product.id {
case "subscription.standard":
self = .standard
case "subscription.premium":
self = .premium
case "subscription.pro":
self = .pro
default:
self = .notEntitled
}
}
}
public static func < (lhs: Self, rhs: Self) -> Bool {
// 订阅组级别按降序排列
return lhs.rawValue > rhs.rawValue
}
}
Store类的核心属性
Store
类是一个ObservableObject
,用于管理应用内所有产品和购买状态。
- Name
主要属性
- Type
- concept
- Description
- 产品分类列表
- 已购买产品状态
- 订阅状态跟踪
- 产品ID到表情映射
- Name
发布属性
- Type
- meaning
- Description
所有带有
@Published
标记的属性会在值变化时自动通知UI更新,实现数据与界面的绑定。
Store类属性定义
class Store: ObservableObject {
// 产品分类
@Published private(set) var cars: [Product]
@Published private(set) var fuel: [Product]
@Published private(set) var subscriptions: [Product]
@Published private(set) var nonRenewables: [Product]
// 已购买产品状态
@Published private(set) var purchasedCars: [Product] = []
@Published private(set) var purchasedNonRenewableSubscriptions: [Product] = []
@Published private(set) var purchasedSubscriptions: [Product] = []
@Published private(set) var subscriptionGroupStatus: Product.SubscriptionInfo.Status?
// 交易监听任务
var updateListenerTask: Task<Void, Error>? = nil
// 产品ID到表情映射字典
private let productIdToEmoji: [String: String]
init() {
// 从配置文件加载产品ID到表情的映射
productIdToEmoji = Store.loadProductIdToEmojiData()
// 初始化空产品列表,然后异步执行产品请求来填充它们
cars = []
fuel = []
subscriptions = []
nonRenewables = []
// 尽可能早地启动交易监听器,以免错过任何交易
updateListenerTask = listenForTransactions()
Task {
// 在商店初始化期间,从App Store请求产品
await requestProducts()
// 交付用户购买的产品
await updateCustomerProductStatus()
}
}
deinit {
// 在类销毁时取消交易监听任务
updateListenerTask?.cancel()
}
}
产品配置加载
Store
类从配置文件中加载产品ID到表情符号的映射,这是一种将产品与视觉元素关联的简洁方式。
- Name
实现细节
- Type
- steps
- Description
- 读取Products.plist文件
- 解析为字典数据
- 提供产品ID到表情的映射
- Name
应用场景
- Type
- meaning
- Description
在UI中展示产品时,可以使用对应的表情符号来增强视觉效果。
产品配置加载
static func loadProductIdToEmojiData() -> [String: String] {
guard let path = Bundle.main.path(forResource: "Products", ofType: "plist"),
let plist = FileManager.default.contents(atPath: path),
let data = try? PropertyListSerialization.propertyList(from: plist, format: nil) as? [String: String] else {
return [:]
}
return data
}
// 获取产品对应的表情符号
func emoji(for productId: String) -> String {
return productIdToEmoji[productId]!
}
// 按价格排序产品
func sortByPrice(_ products: [Product]) -> [Product] {
products.sorted(by: { return $0.price < $1.price })
}
交易监听实现
StoreKit 2提供了一个强大的交易监听机制,可以捕获所有交易更新,包括应用启动时的恢复购买。
- Name
关键功能
- Type
- steps
- Description
- 创建后台任务监听交易
- 验证每个交易的真实性
- 更新用户产品状态
- 完成交易处理
- Name
注意事项
- Type
- notes
- Description
- 应尽早启动监听器
- 必须验证每个交易
- 必须完成(finish)每个交易
- 处理验证失败的情况
交易监听实现
func listenForTransactions() -> Task<Void, Error> {
return Task.detached {
// 迭代处理所有不是直接来自`purchase()`调用的交易
for await result in Transaction.updates {
do {
let transaction = try self.checkVerified(result)
// 向用户交付产品
await self.updateCustomerProductStatus()
// 始终完成交易
await transaction.finish()
} catch {
// StoreKit有一个验证失败的交易。不要向用户交付内容
print("Transaction failed verification.")
}
}
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
// 检查JWS是否通过StoreKit验证
switch result {
case .unverified:
// StoreKit解析了JWS,但验证失败
throw StoreError.failedVerification
case .verified(let safe):
// 结果已验证。返回解包的值
return safe
}
}
产品请求实现
从App Store请求产品信息是内购实现的第一步,StoreKit 2使用异步API简化了这一过程。
- Name
实现步骤
- Type
- steps
- Description
- 使用产品ID请求产品信息
- 根据产品类型分类
- 按价格排序产品
- 更新UI显示
- Name
产品类型
- Type
- concept
- Description
- 消耗型(.consumable):如游戏中的燃料
- 非消耗型(.nonConsumable):如游戏中的车辆
- 自动续期订阅(.autoRenewable):如高级会员
- 非续期订阅(.nonRenewable):如一年期会员
产品请求实现
@MainActor
func requestProducts() async {
do {
// 使用`Products.plist`文件定义的标识符从App Store请求产品
let storeProducts = try await Product.products(for: productIdToEmoji.keys)
var newCars: [Product] = []
var newSubscriptions: [Product] = []
var newNonRenewables: [Product] = []
var newFuel: [Product] = []
// 根据类型将产品分类
for product in storeProducts {
switch product.type {
case .consumable:
newFuel.append(product)
case .nonConsumable:
newCars.append(product)
case .autoRenewable:
newSubscriptions.append(product)
case .nonRenewable:
newNonRenewables.append(product)
default:
// 忽略此产品
print("Unknown product.")
}
}
// 按价格从低到高对每个产品类别进行排序,以更新商店
cars = sortByPrice(newCars)
subscriptions = sortByPrice(newSubscriptions)
nonRenewables = sortByPrice(newNonRenewables)
fuel = sortByPrice(newFuel)
} catch {
print("Failed product request from the App Store server. \(error)")
}
}
购买流程实现
StoreKit 2简化了购买流程,使用异步API处理整个购买过程,包括验证和交付。
- Name
购买步骤
- Type
- steps
- Description
- 发起产品购买请求
- 处理购买结果
- 验证交易
- 更新用户产品状态
- 完成交易
- Name
结果处理
- Type
- notes
- Description
- 成功:验证并交付产品
- 用户取消:返回nil
- 待处理:等待进一步处理
- 其他情况:返回nil
购买流程实现
func purchase(_ product: Product) async throws -> Transaction? {
// 开始购买用户选择的`Product`
let result = try await product.purchase()
switch result {
case .success(let verification):
// 检查交易是否已验证。如果没有,
// 此函数会重新抛出验证错误
let transaction = try checkVerified(verification)
// 交易已验证。向用户交付内容
await updateCustomerProductStatus()
// 始终完成交易
await transaction.finish()
return transaction
case .userCancelled, .pending:
return nil
default:
return nil
}
}
func isPurchased(_ product: Product) async throws -> Bool {
// 确定用户是否购买了给定产品
switch product.type {
case .nonRenewable:
return purchasedNonRenewableSubscriptions.contains(product)
case .nonConsumable:
return purchasedCars.contains(product)
case .autoRenewable:
return purchasedSubscriptions.contains(product)
default:
return false
}
}
用户产品状态更新
这个方法是整个Store
类的核心,负责确定用户拥有哪些产品的权限,并更新UI状态。
- Name
实现功能
- Type
- steps
- Description
- 获取用户当前所有权限
- 处理不同类型的产品
- 验证非续期订阅的有效期
- 更新订阅组状态
- Name
特殊处理
- Type
- notes
- Description
- 非消耗型产品:永久拥有
- 非续期订阅:需手动计算过期时间
- 自动续期订阅:检查订阅状态
- 家庭共享:获取最高服务级别
用户产品状态更新
@MainActor
func updateCustomerProductStatus() async {
var purchasedCars: [Product] = []
var purchasedSubscriptions: [Product] = []
var purchasedNonRenewableSubscriptions: [Product] = []
// 迭代用户购买的所有产品
for await result in Transaction.currentEntitlements {
do {
// 检查交易是否已验证。如果没有,捕获`failedVerification`错误
let transaction = try checkVerified(result)
// 检查交易的`productType`并从商店获取相应的产品
switch transaction.productType {
case .nonConsumable:
if let car = cars.first(where: { $0.id == transaction.productID }) {
purchasedCars.append(car)
}
case .nonRenewable:
if let nonRenewable = nonRenewables.first(where: { $0.id == transaction.productID }),
transaction.productID == "nonRenewing.standard" {
// 非续期订阅没有固有的过期日期,所以`Transaction.currentEntitlements`
// 在用户购买后始终包含它们
// 此应用将此非续期订阅的过期日期定义为购买后一年
// 如果当前日期在购买日期的一年内,用户仍有权使用此产品
let currentDate = Date()
let expirationDate = Calendar(identifier: .gregorian).date(byAdding: DateComponents(year: 1),
to: transaction.purchaseDate)!
if currentDate < expirationDate {
purchasedNonRenewableSubscriptions.append(nonRenewable)
}
}
case .autoRenewable:
if let subscription = subscriptions.first(where: { $0.id == transaction.productID }) {
purchasedSubscriptions.append(subscription)
}
default:
break
}
} catch {
print()
}
}
// 使用购买的产品更新商店信息
self.purchasedCars = purchasedCars
self.purchasedNonRenewableSubscriptions = purchasedNonRenewableSubscriptions
// 使用自动续期订阅产品更新商店信息
self.purchasedSubscriptions = purchasedSubscriptions
// 检查`subscriptionGroupStatus`以了解自动续期订阅状态,确定客户
// 是新客户(从未订阅)、活跃客户还是非活跃客户(订阅已过期)
// 此应用只有一个订阅组,因此subscriptions数组中的产品都属于同一组
// 客户只能订阅订阅组中的一个产品
// `product.subscription.status`返回的状态适用于整个订阅组
subscriptionGroupStatus = try? await subscriptions.first?.subscription?.status.max { lhs, rhs in
// 由于此应用支持家庭共享,可能有多个家庭成员的不同状态
// 订阅者有权获得服务级别最高的状态
let lhsEntitlement = entitlement(for: lhs)
let rhsEntitlement = entitlement(for: rhs)
return lhsEntitlement < rhsEntitlement
}
}
订阅权限管理
这个方法用于确定用户的订阅服务级别,特别是在有多个订阅选项和家庭共享的情况下。
- Name
实现功能
- Type
- steps
- Description
- 检查订阅状态
- 处理过期或撤销的情况
- 获取产品对应的权限级别
- Name
应用场景
- Type
- meaning
- Description
- 多级订阅管理
- 家庭共享权限处理
- 服务级别降级处理
订阅权限管理
// 使用产品ID获取订阅的服务级别
func entitlement(for status: Product.SubscriptionInfo.Status) -> ServiceEntitlement {
// 如果状态是过期的,则客户没有权限
if status.state == .expired || status.state == .revoked {
return .notEntitled
}
// 获取与订阅状态关联的产品
let productID = status.transaction.unsafePayloadValue.productID
guard let product = subscriptions.first(where: { $0.id == productID }) else {
return .notEntitled
}
// 最后,获取此产品对应的权限
return ServiceEntitlement(for: product) ?? .notEntitled
}
StoreKit 2的主要优势
相比传统的StoreKit 1 API,StoreKit 2提供了许多显著的改进。
- Name
技术优势
- Type
- concept
- Description
- 使用Swift并发API
- 内置收据验证
- 简化的交易处理
- 更好的订阅管理
- 更少的样板代码
- Name
实际应用
- Type
- meaning
- Description
- 减少开发时间
- 提高代码安全性
- 简化服务器验证
- 更好的用户体验
StoreKit 2与StoreKit 1对比
// StoreKit 1 (旧API)
// 1. 需要手动实现收据验证
// 2. 使用代理模式处理异步回调
// 3. 需要大量样板代码处理交易队列
// 4. 订阅状态管理复杂
// StoreKit 2 (新API)
// 1. 使用async/await简化异步操作
// 2. 内置收据验证
// 3. 使用Transaction.updates和Transaction.currentEntitlements简化交易处理
// 4. 提供丰富的订阅状态API
// 5. 支持家庭共享
// 示例:StoreKit 2简化的产品请求
let products = try await Product.products(for: ["com.example.product1"])
// 示例:StoreKit 2简化的购买流程
let result = try await product.purchase()
最佳实践与注意事项
在实现StoreKit 2内购时,有一些重要的最佳实践需要遵循。
- Name
关键建议
- Type
- key-points
- Description
- 尽早启动交易监听器
- 始终验证每个交易
- 始终完成(finish)每个交易
- 正确处理不同类型的产品
- 实现恢复购买功能
- Name
常见陷阱
- Type
- notes
- Description
- 忘记完成交易导致重复交付
- 未正确处理非续期订阅过期
- 未考虑家庭共享情况
- 未处理网络错误和离线情况
实现要点与陷阱
// 1. 尽早启动交易监听器
init() {
// ...其他初始化代码...
// 在应用启动时尽早启动,避免错过交易
updateListenerTask = listenForTransactions()
}
// 2. 始终验证交易
let transaction = try checkVerified(verification)
// 3. 始终完成交易
await transaction.finish()
// 4. 正确处理非续期订阅过期
let currentDate = Date()
let expirationDate = Calendar(identifier: .gregorian)
.date(byAdding: DateComponents(year: 1), to: transaction.purchaseDate)!
if currentDate < expirationDate {
// 订阅仍然有效
}
// 5. 处理家庭共享
subscriptionGroupStatus = try? await subscriptions.first?.subscription?.status.max { lhs, rhs in
let lhsEntitlement = entitlement(for: lhs)
let rhsEntitlement = entitlement(for: rhs)
return lhsEntitlement < rhsEntitlement
}