在 StoreKit 框架中,Product
是表示应用内购买商品的核心类型,用于定义用户在应用中可购买的虚拟商品或服务(如消耗型商品、非消耗型商品、自动续期订阅等)。它是 StoreKit 2(iOS 15+)引入的新 API 的一部分,提供了更简洁、安全的操作方式。以下是 Product
的详细介绍:
1. Product 的定义与作用 #
- 表示商品元数据
Product
封装了在 App Store Connect 中配置的商品信息,包括商品 ID、名称、描述、价格等。 - 发起购买流程
通过Product
对象可以触发用户的购买行为,并与Transaction
结合完成交易验证。 - 支持多种商品类型
涵盖消耗型(如游戏金币)、非消耗型(如永久解锁功能)、自动续期订阅等。
2. Product 的结构体 #
Product
是一个结构体(struct
),包含以下关键属性:
核心属性 #
属性 | 类型 | 说明 |
---|---|---|
id | String | 商品的唯一标识符(需与 App Store Connect 中配置一致) |
displayName | String | 本地化的商品名称 |
description | String | 本地化的商品描述 |
price | Decimal | 商品价格(以用户本地货币为单位) |
displayPrice | String | 格式化后的价格字符串(如 “$0.99”) |
type | Product.ProductType | 商品类型(消耗型、非消耗型、自动续期订阅等) |
订阅相关属性(仅限自动续期订阅) #
属性 | 类型 | 说明 |
---|---|---|
subscription | Product.SubscriptionInfo? | 订阅的详细信息(周期、促销优惠等) |
3. 商品类型(ProductType) #
Product.ProductType
枚举定义了商品的类型:
public enum ProductType: Hashable, Sendable {
case consumable // 消耗型(如游戏金币)
case nonConsumable // 非消耗型(如永久功能解锁)
case autoRenewable // 自动续期订阅
case nonRenewable // 非续期订阅(已弃用,不推荐使用)
}
4. 如何获取 Product 列表 #
(1) 获取单个商品 #
let productIDs = ["com.yourapp.premium_subscription_monthly"]
let products = try await Product.products(for: productIDs)
guard let product = products.first else { return }
(2) 获取多个商品 #
let productIDs = [
"com.yourapp.gold_coins_100",
"com.yourapp.premium_subscription_monthly"
]
let products = try await Product.products(for: productIDs)
(3) 处理错误 #
do {
let products = try await Product.products(for: productIDs)
} catch {
print("获取商品失败: \(error)")
// 常见错误:无效商品 ID、网络问题、未配置商品等
}
5. 发起购买 #
通过 Product
的 purchase()
方法触发购买流程,并处理结果:
func purchaseProduct(_ product: Product) async throws -> Transaction? {
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
await transaction.finish() // 标记交易完成
return transaction
case .unverified(_, let error):
throw error // 交易未通过验证(可能被篡改)
}
case .pending:
// 交易等待中(如需要家长批准)
return nil
case .userCancelled:
// 用户取消购买
return nil
@unknown default:
return nil
}
}
6. 订阅信息的处理 #
对于自动续期订阅商品,可通过 subscription
属性获取详细信息:
if let subscriptionInfo = product.subscription {
// 订阅周期信息
let subscriptionPeriod = subscriptionInfo.subscriptionPeriod
print("周期: \(subscriptionPeriod.value) \(subscriptionPeriod.unit)")
// 促销优惠(如免费试用)
if let promotionalOffer = subscriptionInfo.promotionalOffers.first {
print("促销价格: \(promotionalOffer.price)")
}
}
订阅周期单位(PeriodUnit) #
值 | 说明 |
---|---|
.day | 天 |
.week | 周 |
.month | 月 |
.year | 年 |
7. 本地化与价格格式化 #
自动适配用户本地化
displayName
和description
会根据设备语言显示对应内容(需在 App Store Connect 中配置多语言)。手动格式化价格
使用priceFormatStyle
自定义价格显示:let formattedPrice = product.price.formatted( .currency(code: "USD").precision(.fractionLength(0...2)) )
8. 完整代码示例 #
场景:获取商品列表并显示在 UI 中 #
import StoreKit
class StoreManager: ObservableObject {
@Published var products: [Product] = []
func fetchProducts() async {
let productIDs = [
"com.yourapp.gold_coins_100",
"com.yourapp.premium_subscription_monthly"
]
do {
let products = try await Product.products(for: productIDs)
await MainActor.run {
self.products = products
}
} catch {
print("商品加载失败: \(error)")
}
}
}
// 在 SwiftUI 中显示商品列表
struct ProductListView: View {
@ObservedObject var storeManager: StoreManager
var body: some View {
List(storeManager.products, id: \.id) { product in
VStack(alignment: .leading) {
Text(product.displayName)
Text(product.description)
Text(product.displayPrice)
.bold()
}
.onTapGesture {
Task { try await storeManager.purchaseProduct(product) }
}
}
}
}
9. 注意事项 #
商品 ID 一致性
确保代码中的商品 ID 与 App Store Connect 中配置的完全一致。网络依赖
Product.products(for:)
需要网络连接,首次调用可能较慢(后续会缓存)。沙盒测试
- 使用沙盒环境测试时,商品价格显示为 “¥XX.XX”(无货币符号本地化)。
- 自动续期订阅在沙盒中会加速过期(测试周期缩短)。
交易完成
购买后必须调用Transaction.finish()
,否则交易会保持在队列中导致重复购买。未配置商品的错误
如果传入未在 App Store Connect 中配置的商品 ID,Product.products(for:)
会忽略该 ID 且不报错。
10. 与传统 StoreKit 1 的区别 #
特性 | StoreKit 1 (SKProduct) | StoreKit 2 (Product) |
---|---|---|
获取商品 | 通过 SKProductsRequest 委托回调 | 使用 async/await 直接返回 |
价格处理 | 需手动格式化 | 提供 displayPrice 自动格式化 |
订阅信息 | 无直接支持 | 通过 subscription 属性提供详细信息 |
代码复杂度 | 高(需管理请求和回调) | 低(简洁的异步 API) |
11. 常见问题与解决 #
问题:无法获取商品列表
排查步骤:- 检查商品 ID 是否正确且已在 App Store Connect 中提交审核。
- 确认网络连接正常,且未使用 VPN 或防火墙阻止 App Store 通信。
- 确保 Bundle ID 与 App Store Connect 中的应用匹配。
问题:价格显示为占位符(如 “¥XX.XX”)
原因:在沙盒环境中,某些地区可能不显示具体货币符号。
解决:使用真实设备测试或检查 App Store Connect 的价格配置。
12. 官方文档参考 #
通过合理使用 Product
,开发者可以高效管理应用内购买商品,提供流畅的用户购买体验。建议结合 Transaction
和 AppStore.sync()
确保交易状态的实时性和准确性。