使用 StoreKit 2 实现、审查或改进应用内购买和订阅。适用于使用 SubscriptionStoreView 或 ProductView 构建付费墙、使用 Product 和 Transaction API 处理交易、验证权益、处理购买流程(消耗型、非消耗型、自动续期)、实现优惠码或促销/赢回/ introductory 优惠、管理订阅状态和续期状态、使用配置文件设置 StoreKit 测试,或集成家庭共享、请求购买、退款处理和账单重试逻辑。
StoreKit 2 应用内购买和订阅
使用 StoreKit 2 实现应用内购买、订阅、付费墙和 StoreKit 测试。使用基于 Swift 的现代 Product、Transaction、PurchaseAction、StoreView 和 SubscriptionStoreView API。除非需要支持旧版操作系统,否则避免使用原始的应用内购买 API(SKProduct、SKPaymentQueue)。
StoreKit 视图会自动发起购买。对于自定义控件,在 SwiftUI 中使用 PurchaseAction,在 UIKit/AppKit 中使用 purchase(confirmIn:options:),在 watchOS 中使用 product.purchase(options:)。
目录
- 产品类型
- 加载产品
- 购买流程
- Transaction.updates 监听器
- 权益检查
- SubscriptionStoreView (iOS 17+)
- StoreView (iOS 17+)
- 订阅状态检查
- 恢复购买
- 应用交易(应用购买验证)
- 购买选项
- SwiftUI 购买回调
- 常见错误
- 审查清单
- 参考资料
产品类型
| 类型 | 枚举值 | 行为 |
|---|---|---|
| 消耗型 | .consumable |
一次性使用,可重复购买(宝石、金币) |
| 非消耗型 | .nonConsumable |
永久购买一次(高级解锁) |
| 自动续期 | .autoRenewable |
自动续费的定期计费 |
| 非续期 | .nonRenewing |
限时访问,不自动续费 |
加载产品
将产品 ID 定义为常量。使用 Product.products(for:) 获取产品。
import StoreKit
enum ProductID {
static let premium = "com.myapp.premium"
static let gems100 = "com.myapp.gems100"
static let monthlyPlan = "com.myapp.monthly"
static let yearlyPlan = "com.myapp.yearly"
static let all: [String] = [premium, gems100, monthlyPlan, yearlyPlan]
}
let products = try await Product.products(for: ProductID.all)
for product in products {
print("\(product.displayName): \(product.displayPrice)")
}
购买流程
对于标准付费墙,优先使用 StoreKit 视图,因为它们会自动处理购买、恢复购买和显示政策控件。对于自定义 SwiftUI 购买按钮,优先使用环境中的 PurchaseAction。在 watchOS 上使用直接的 product.purchase(options:),在 UIKit 或 AppKit 中使用 purchase(confirmIn:options:)。始终处理每个 PurchaseResult,在访问前验证,持久化交付,然后完成。
@Environment(\.purchase) private var purchase
func purchaseProduct(_ product: Product) async throws {
let result = try await purchase(product, options: [
.appAccountToken(userAccountToken)
])
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await deliverContent(for: transaction)
await transaction.finish()
case .userCancelled:
break
case .pending:
// 请求购买或延迟批准:显示待处理 UI,暂不解锁。
showPendingApprovalMessage()
@unknown default:
break
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let value): return value
case .unverified(_, let error): throw error
}
}
Transaction.updates 监听器
在应用启动时启动,而不是在付费墙出现时。捕获来自其他设备的购买、家庭共享更改、续期、请求购买批准、退款、撤销以及 Apple 在启动后立即发出的未完成交易。保持任务在应用生命周期内持续。
@main
struct MyApp: App {
private let transactionListener: Task<Void, Never>
init() {
transactionListener = Self.listenForTransactions()
}
var body: some Scene {
WindowGroup { ContentView() }
}
static func listenForTransactions() -> Task<Void, Never> {
Task(priority: .background) {
for await result in Transaction.updates {
guard case .verified(let transaction) = result else { continue }
await StoreManager.shared.updateEntitlements()
await transaction.finish()
}
}
}
}
权益检查
Transaction.currentEntitlements 会发出非消耗型、活跃或宽限期内的自动续期订阅,以及最新的非续期订阅交易(包括已完成的)。它排除消耗型以及已退款或已撤销的产品。单独跟踪消耗型的交付,并在授予访问权限前对非续期订阅应用应用的过期策略。
@Observable
@MainActor
class StoreManager {
static let shared = StoreManager()
var purchasedProductIDs: Set<String> = []
var isPremium: Bool { purchasedProductIDs.contains(ProductID.premium) }
func updateEntitlements() async {
var purchased = Set<String>()
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result,
transaction.revocationDate == nil {
if transaction.productType == .nonRenewing,
transaction.expirationDate.map({ $0 <= .now }) ?? true {
continue
}
purchased.insert(transaction.productID)
}
}
purchasedProductIDs = purchased
}
}
SwiftUI .currentEntitlementTask 修饰符
struct PremiumGatedView: View {
@State private var state: EntitlementTaskState<VerificationResult<Transaction>?> = .loading
var body: some View {
Group {
switch state {
case .loading: ProgressView()
case .failure: PaywallView()
case .success(.some(.verified(let transaction))) where transaction.revocationDate == nil:
PremiumContentView()
case .success:
PaywallView()
}
}
.currentEntitlementTask(for: ProductID.premium) { state in
self.state = state
}
}
}
SubscriptionStoreView (iOS 17+)
用于订阅付费墙的内置 SwiftUI 视图。自动处理产品加载、购买 UI 和恢复购买。
SubscriptionStoreView(groupID: "YOUR_GROUP_ID")
.subscriptionStoreControlStyle(.prominentPicker)
.subscriptionStoreButtonLabel(.multiline)
.storeButton(.visible, for: .restorePurchases)
.storeButton(.visible, for: .redeemCode)
.subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
.subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
.onInAppPurchaseCompletion { product, result in
if case .success(.success(.verified(let transaction))) = result {
await deliverContent(for: transaction)
await transaction.finish()
}
}
自定义营销内容
使用 SubscriptionStoreView 控件样式 中的容器背景和头部模式。
分层布局
使用 SubscriptionOptionGroup、SubscriptionOptionSection 或 SubscriptionPeriodGroupSet 组织 iOS 18+ 选项;参见 订阅组管理。
StoreView (iOS 17+)
展示多个产品,包含本地化名称、价格和购买按钮。
StoreView(ids: [ProductID.gems100, ProductID.premium], prefersPromotionalIcon: true)
.productViewStyle(.large)
.storeButton(.visible, for: .restorePurchases)
.onInAppPurchaseCompletion { product, result in
if case .success(.success(.verified(let transaction))) = result {
await deliverContent(for: transaction)
await transaction.finish()
}
}
单个产品的 ProductView
ProductView(id: ProductID.premium) { iconPhase in
switch iconPhase {
case .success(let image): image.resizable().scaledToFit()
case .loading: ProgressView()
default: Image(systemName: "star.fill")
}
}
.productViewStyle(.large)
订阅状态检查
func checkSubscriptionActive(groupID: String) async throws -> Bool {
let statuses = try await Product.SubscriptionInfo.status(for: groupID)
for status in statuses {
guard case .verified = status.renewalInfo,
case .verified = status.transaction else { continue }
if status.state == .subscribed || status.state == .inGracePeriod {
return true
}
}
return false
}
续期状态
| 状态 | 含义 |
|---|---|
.subscribed |
订阅活跃 |
.expired |
订阅已过期 |
.inBillingRetryPeriod |
支付失败,Apple 正在重试 |
.inGracePeriod |
支付失败,但宽限期内仍可访问 |
.revoked |
Apple 已退款或撤销订阅 |
恢复购买
StoreKit 2 通过 Transaction.currentEntitlements 处理恢复。添加恢复按钮或显式调用 AppStore.sync()。
func restorePurchases() async throws {
try await AppStore.sync()
await StoreManager.shared.updateEntitlements()
}
在商店视图上:.storeButton(.visible, for: .restorePurchases)
应用交易(应用购买验证)
验证应用安装的合法性。用于商业模式变更或检测篡改安装(iOS 16+)。
func verifyAppPurchase() async {
do {
let result = try await AppTransaction.shared
switch result {
case .verified(let appTransaction):
let originalVersion = appTransaction.originalAppVersion
let purchaseDate = appTransaction.originalPurchaseDate
// 针对在订阅模式前付费用户的迁移逻辑
case .unverified:
// 可能被篡改——酌情限制功能
break
}
} catch { /* 无法获取应用交易 */ }
}
购买选项
// 用于服务器端对账的应用账户令牌
try await product.purchase(options: [.appAccountToken(UUID())])
// 消耗型数量
try await product.purchase(options: [.quantity(5)])
// 在沙盒中模拟请求购买
try await product.purchase(options: [.simulatesAskToBuyInSandbox(true)])
SwiftUI 购买回调
.onInAppPurchaseStart { product in
await analytics.trackPurchaseStarted(product.id)
}
.onInAppPurchaseCompletion { product, result in
if case .success(.success(.verified(let transaction))) = result {
await deliverContent(for: transaction)
await transaction.finish()
}
}
.inAppPurchaseOptions { product in
[.appAccountToken(userAccountToken)]
}
常见错误
1. 未在应用启动时启动 Transaction.updates
// 错误:没有监听器——错过续期、退款、请求购买批准
@main struct MyApp: App {
var body: some Scene { WindowGroup { ContentView() } }
}
// 正确:在 App init 中启动监听器(参见上面的 Transaction.updates 部分)
2. 忘记调用 transaction.finish()
// 错误:从未完成——永远出现在未完成队列中
let transaction = try checkVerified(verification)
unlockFeature(transaction.productID)
// 正确:先持久化交付,然后完成。如果交付失败,暂不完成。
let transaction = try checkVerified(verification)
try await recordDelivery(transaction)
await transaction.finish()
3. 忽略验证结果
// 错误:使用未验证的交易——安全风险
let transaction = verification.unsafePayloadValue
// 正确:使用前验证
let transaction = try checkVerified(verification)
4. 在新的 StoreKit 2 代码中使用原始的应用内购买 API
// 避免:原始的应用内购买 API
let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])
SKPaymentQueue.default().add(payment)
// 推荐:StoreKit 2
let products = try await Product.products(for: ["com.app.premium"])
let result = try await product.purchase()
5. 未检查 revocationDate
// 错误:对已退款购买授予访问权限
if case .verified(let transaction) = result {
purchased.insert(transaction.productID)
}
// 正确:跳过已撤销的交易
if case .verified(let transaction) = result, transaction.revocationDate == nil {
purchased.insert(transaction.productID)
}
6. 硬编码价格
// 错误:对其他货币和地区不正确
Text("Buy Premium for $4.99")
// 正确:使用 Product 的本地化价格
Text("Buy \(product.displayName) for \(product.displayPrice)")
7. 未处理 .pending 购买结果
// 错误:静默丢弃待处理的请求购买
default: break
// 正确:说明批准待处理;仅在 Transaction.updates 后解锁
case .pending:
showPendingApprovalMessage()
8. 仅在启动时检查一次权益
// 错误:只检查一次,从不更新
func appDidFinish() { Task { await updateEntitlements() } }
// 正确:在 Transaction.updates 和返回前台时重新检查
// Transaction.updates 监听器处理会话中的更改。
// 同时在内容视图上使用 .task { await storeManager.updateEntitlements() }。
9. 缺少恢复购买按钮
// 错误:没有恢复选项——有被 App Store 拒绝的风险
SubscriptionStoreView(groupID: "group_id")
// 正确
SubscriptionStoreView(groupID: "group_id")
.storeButton(.visible, for: .restorePurchases)
10. 订阅视图没有政策链接
// 错误:没有条款或隐私政策
SubscriptionStoreView(groupID: "group_id")
// 正确
SubscriptionStoreView(groupID: "group_id")
.subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
.subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
审查清单
- [ ]
Transaction.updates监听器在应用启动时于 App init 中启动 - [ ] 所有交易在授予访问权限前已验证
- [ ] 仅在持久化内容交付后调用
transaction.finish() - [ ] 排除已撤销/已退款的交易,并更新权益状态
- [ ]
.pending结果显示请求购买/延迟批准反馈 - [ ] 恢复购买按钮在付费墙和商店视图上可见
- [ ] 订阅视图上包含服务条款和隐私政策链接
- [ ] 使用
product.displayPrice显示价格,绝不硬编码 - [ ] 订阅条款(价格、时长、续期)清晰显示
- [ ] 免费试用期后明确显示定价
- [ ] 除非需要支持旧版操作系统,否则不使用原始的应用内购买 API(
SKProduct、SKPaymentQueue) - [ ] 产品 ID 定义为常量,而非分散的字符串
- [ ] StoreKit 测试涵盖促销优惠、赢回、优惠码、请求购买、续期、退款和撤销
- [ ] 在 Transaction.updates 和应用进入前台时重新检查权益
- [ ] 如果适用,服务器端验证使用
jwsRepresentation - [ ] 消耗型及时交付并完成
- [ ] 当跨并发边界共享时,交易观察者类型和产品模型类型是
Sendable
参考资料
- 参见 references/app-review-guidelines.md 了解 IAP 规则(指南 3.1.1)、订阅显示要求和拒绝预防。
- 参见 references/storekit-advanced.md 了解订阅控件样式、优惠管理、测试模式和高级订阅处理。
- 对于提交、隐私、元数据、截图和拒绝风险审计,使用
app-store-review。 - 对于关键词、截图说明、排名和转化策略,使用
app-store-optimization。 - Apple 官方文档:选择 StoreKit API、Transaction.updates、Transaction.currentEntitlements、SubscriptionStoreView 和 PurchaseAction。






