StoreKit — Product
This article is extracted from the chat log with AI. Please identify it with caution.

在 StoreKit 框架中,Product 是表示应用内购买商品的核心类型,用于定义用户在应用中可购买的虚拟商品或服务(如消耗型商品、非消耗型商品、自动续期订阅等)。它是 StoreKit 2(iOS 15+)引入的新 API 的一部分,提供了更简洁、安全的操作方式。以下是 Product 的详细介绍:


1. Product 的定义与作用 #

  • 表示商品元数据
    Product 封装了在 App Store Connect 中配置的商品信息,包括商品 ID、名称、描述、价格等。
  • 发起购买流程
    通过 Product 对象可以触发用户的购买行为,并与 Transaction 结合完成交易验证。
  • 支持多种商品类型
    涵盖消耗型(如游戏金币)、非消耗型(如永久解锁功能)、自动续期订阅等。

2. Product 的结构体 #

Product 是一个结构体(struct),包含以下关键属性:

核心属性 #

属性类型说明
idString商品的唯一标识符(需与 App Store Connect 中配置一致)
displayNameString本地化的商品名称
descriptionString本地化的商品描述
priceDecimal商品价格(以用户本地货币为单位)
displayPriceString格式化后的价格字符串(如 “$0.99”)
typeProduct.ProductType商品类型(消耗型、非消耗型、自动续期订阅等)

订阅相关属性(仅限自动续期订阅) #

属性类型说明
subscriptionProduct.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. 发起购买 #

通过 Productpurchase() 方法触发购买流程,并处理结果:

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. 本地化与价格格式化 #

  • 自动适配用户本地化
    displayNamedescription 会根据设备语言显示对应内容(需在 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. 注意事项 #

  1. 商品 ID 一致性
    确保代码中的商品 ID 与 App Store Connect 中配置的完全一致。

  2. 网络依赖
    Product.products(for:) 需要网络连接,首次调用可能较慢(后续会缓存)。

  3. 沙盒测试

    • 使用沙盒环境测试时,商品价格显示为 “¥XX.XX”(无货币符号本地化)。
    • 自动续期订阅在沙盒中会加速过期(测试周期缩短)。
  4. 交易完成
    购买后必须调用 Transaction.finish(),否则交易会保持在队列中导致重复购买。

  5. 未配置商品的错误
    如果传入未在 App Store Connect 中配置的商品 ID,Product.products(for:) 会忽略该 ID 且不报错。


10. 与传统 StoreKit 1 的区别 #

特性StoreKit 1 (SKProduct)StoreKit 2 (Product)
获取商品通过 SKProductsRequest 委托回调使用 async/await 直接返回
价格处理需手动格式化提供 displayPrice 自动格式化
订阅信息无直接支持通过 subscription 属性提供详细信息
代码复杂度高(需管理请求和回调)低(简洁的异步 API)

11. 常见问题与解决 #

  • 问题:无法获取商品列表
    排查步骤

    1. 检查商品 ID 是否正确且已在 App Store Connect 中提交审核。
    2. 确认网络连接正常,且未使用 VPN 或防火墙阻止 App Store 通信。
    3. 确保 Bundle ID 与 App Store Connect 中的应用匹配。
  • 问题:价格显示为占位符(如 “¥XX.XX”)
    原因:在沙盒环境中,某些地区可能不显示具体货币符号。
    解决:使用真实设备测试或检查 App Store Connect 的价格配置。


12. 官方文档参考 #

通过合理使用 Product,开发者可以高效管理应用内购买商品,提供流畅的用户购买体验。建议结合 TransactionAppStore.sync() 确保交易状态的实时性和准确性。

本文共 1740 字,创建于 May 5, 2025
相关标签: Xcode, ByAI, StoreKit