Highlight
StoreKit 2 在 WWDC2023 新增推广购买管理 API、Transaction/RenewalInfo 数据模型扩展、SwiftUI 专用商品展示视图(ProductView/StoreView/SubscriptionStoreView),以及 Xcode 15 中支持多设备离线调试、模拟错误注入和自定义订阅续费速率的 StoreKit Testing 工具链。
核心内容
推广购买的完整链路
用户在 App Store 产品页看到推广的内购项目,点击购买后,应用需要监听这个意图并完成交易。以前开发者只能被动等待,现在可以主动管理推广产品的展示顺序和可见性。
用户在 App Store 购买推广产品时,应用可能还没准备好(比如在关卡中)。PurchaseIntent 支持延迟处理,保存意图稍后完成。已购买的产品可以隐藏,游戏关卡进度可以动态调整展示顺序。
数据模型终于够用了
Transaction 模型新增了 storefront(商店地区)、storefrontCountryCode(国家代码)和 reason(购买原因:用户主动购买还是自动续订)。RenewalInfo 新增了 nextRenewalDate(下次续订日期)。
这些字段解决了长期痛点:判断用户来自哪个地区商店、区分首次购买和续订、提前告知用户下次扣费时间。
测试不再是黑箱
Xcode 15 的 StoreKit Testing 支持在设备未运行应用时直接创建交易,模拟一年前的订阅购买并观察自动续订。可以注入网络错误、产品不可用等异常,验证应用的容错逻辑。订阅续订速率可以调到每分钟一次,快速验证长期订阅状态。
详细内容
监听推广购买意图
(01:42)
import StoreKit
let promotedPurchasesListener = Task {
for await promotion in PurchaseIntent.intents {
let product = promotion.product
do {
let result = try await product.purchase()
// 处理购买结果
} catch {
// 处理错误,或保存意图稍后重试
}
}
}
关键点:
PurchaseIntent.intents是 async sequence,每次用户在 App Store 点击推广购买时产生新值- 应用未运行时系统会保留意图,启动后立刻收到
- 可以保存
promotion对象,在用户合适的时机调用purchase() - 需要在应用生命周期内保持 listener 运行
管理推广产品顺序和可见性
(02:57)
import StoreKit
// 查询当前推广顺序
do {
let promotions = try await Product.PromotionInfo.currentOrder
if promotions.isEmpty {
// 没有本地覆盖,使用 App Store Connect 配置的顺序
}
for promotion in promotions {
let productID = promotion.productID
let visibility = promotion.visibility
}
} catch {
// 处理错误
}
// 设置自定义顺序
let newOrder = ["acorns.individual", "nectar.cup", "sunflowerseeds.pile"]
do {
try await Product.PromotionInfo.updateProductOrder(byID: newOrder)
} catch { }
// 隐藏特定产品
do {
try await Product.PromotionInfo.updateProductVisibility(.hidden, for: "acorns.individual")
} catch { }
// 或通过对象修改
var firstPromotion = promotions.first!
firstPromotion.visibility = .hidden
try await firstPromotion.update()
关键点:
currentOrder为空表示没有本地覆盖,使用 App Store Connect 的默认配置visibility有三种状态:.visible、.hidden、.default(跟随 App Store Connect 设置)- 修改顺序和可见性只影响当前设备
- 适合根据游戏进度、用户状态动态调整推广展示
Transaction 和 RenewalInfo 新字段
(05:00)
// Transaction 新增字段
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
let storefront = transaction.storefront
let countryCode = transaction.storefrontCountryCode
let reason = transaction.reason // .purchase 或 .renewal
print("商店: \(storefront), 原因: \(reason)")
}
}
// RenewalInfo 新增字段
if let subscriptionInfo = product.subscription {
let renewalInfo = try await subscriptionInfo.status
for status in renewalInfo {
if case .verified(let info) = status.renewalInfo {
if let nextDate = info.nextRenewalDate {
print("下次续订: \(nextDate)")
}
}
}
}
关键点:
storefront和storefrontCountryCode帮助判断用户所在地区reason区分用户主动购买(.purchase)和自动续订(.renewal)nextRenewalDate在 RenewalInfo 中,告知下次续订处理时间- Xcode 15 构建的应用可用,大部分字段向前兼容旧版系统
处理账单问题消息
(06:12)
for await message in Message.messages {
switch message.reason {
case .billingIssue:
// 订阅续订失败,显示账单问题 sheet
// StoreKit 自动展示,应用可选择推迟或抑制
await message.display()
default:
break
}
}
关键点:
.billingIssue在 iOS 16.4 引入,订阅因账单问题续订失败时触发- StoreKit 自动展示 Billing Problem sheet,用户无需离开应用即可解决
- 应用可以推迟消息(用户正在交互流程中)或抑制消息
- Sandbox 环境已启用,可提前测试
SwiftUI 商品展示视图
(08:32)
import SwiftUI
import StoreKit
// 单个商品视图
struct BirdFoodShop: View {
let productID: String
var body: some View {
ProductView(id: productID) {
BirdFoodProductIcon(for: productID)
}
.productViewStyle(.large)
}
}
// 商店视图(多个商品)
struct StorePage: View {
let productIDs: [String]
var body: some View {
StoreView(ids: productIDs) { product in
BirdFoodIcon(productID: product.id)
}
}
}
// 订阅商店视图
struct SubscriptionPage: View {
let groupID: String
var body: some View {
SubscriptionStoreView(groupID: groupID)
}
}
// 管理订阅 sheet
.manageSubscriptionsSheet(
isPresented: $showManageSheet,
subscriptionGroupID: groupID // 直接跳转到指定订阅组
)
关键点:
ProductView自动加载商品信息,显示本地化标题、描述和促销图片StoreView展示商品集合,几行代码搭建完整商店SubscriptionStoreView展示订阅组的所有等级,只需subscriptionGroupID.manageSubscriptionsSheet新增subscriptionGroupID参数,直接跳转到指定组- 所有视图支持 SwiftUI 标准自定义,如背景、字体等
Xcode Transaction Manager 离线调试
(11:12)
Xcode 15 的 Transaction Manager 支持:
- 查看所有连接设备和模拟器上的 StoreKit 应用交易记录
- 无需运行应用即可查看历史购买
- 直接从 Mac 创建新购买,配置购买参数(数量、优惠码、购买日期)
- 支持订阅的自动续订配置和历史回溯
操作路径:Debug > StoreKit > Manage Transactions
模拟 StoreKit 错误
(18:54)
在 StoreKit Configuration 文件的 Configuration Settings 中:
- 勾选 Purchase API 的模拟错误选项
- 选择错误类型,如
.productUnavailable(用户切换商店) - 配置自动保存并同步到运行中的设备
- 无需重新运行应用即可测试
支持的 API 和错误类型包括:加载产品、购买、收据验证、交易验证、退款请求等。
单元测试中的模拟购买和错误
(21:09)
import StoreKit
import StoreKitTest
import XCTest
func testSubscriptionRenewal() async throws {
let session = try SKTestSession(configurationFileNamed: "Store")
// 模拟一年前的订阅购买
let oneYear: TimeInterval = 365 * 24 * 60 * 60
let transaction = try await session.buyProduct(
identifier: "birdpass.individual",
options: [.purchaseDate(Date.now - oneYear)]
)
// 验证交易状态
XCTAssertNotNil(transaction)
}
func testLoadProductsError() async throws {
let session = try SKTestSession(configurationFileNamed: "Store")
// 注入网络错误
session.setSimulatedError(.generic(.networkError), forAPI: .loadProducts)
do {
_ = try await Product.products(for: ["product1"])
XCTFail("应该抛出网络错误")
} catch StoreKitError.networkError {
// 预期的错误
}
// 清除模拟错误
session.setSimulatedError(nil, forAPI: .loadProducts)
}
func testFastRenewal() async throws {
let session = try SKTestSession(configurationFileNamed: "Store")
// 每分钟续订一次,快速验证长期订阅状态
session.timeRate = .oneRenewalEveryMinute
let transaction = try await session.buyProduct(identifier: "birdpass.individual")
// 等待观察续订行为
}
关键点:
SKTestSession从 StoreKit Configuration 文件创建测试环境buyProduct(options:)支持purchaseDate等测试专用选项setSimulatedError(_:forAPI:)注入指定 API 的错误timeRate控制订阅续订速度,.oneRenewalEveryMinute最快- 测试结束后清除模拟错误,避免影响其他测试
核心启发
-
动态推广商店
- 做什么:根据用户游戏进度动态调整 App Store 推广产品的展示顺序和可见性
- 为什么值得做:
Product.PromotionInfo允许本地覆盖推广顺序,用户通关后自动展示高级道具,已购买的道具自动隐藏 - 怎么开始:监听
PurchaseIntent.intents处理推广购买,用updateProductOrder和updateProductVisibility根据游戏状态调整
-
订阅状态仪表盘
- 做什么:在应用内展示用户的订阅详情,包括下次续订日期、商店地区、续订原因
- 为什么值得做:
nextRenewalDate让用户提前知道扣费时间,storefrontCountryCode帮助定位地区定价问题 - 怎么开始:遍历
Transaction.currentEntitlements读取storefront和reason,从Product.SubscriptionInfo获取renewalInfo
-
账单问题自助修复
- 做什么:订阅续订失败时主动引导用户修复支付方式,减少流失
- 为什么值得做:
Message.messages中的.billingIssue让应用能在用户离开前解决问题,StoreKit 自动展示修复 sheet - 怎么开始:监听
Message.messages,收到.billingIssue时判断时机,调用message.display()或推迟到合适时机
-
零代码商店页面
- 做什么:用 StoreKit SwiftUI 视图快速搭建应用内购买页面
- 为什么值得做:
StoreView和SubscriptionStoreView几行代码实现完整商店,自动处理加载、展示和购买流程 - 怎么开始:传入商品 ID 数组创建
StoreView,或用SubscriptionStoreView(groupID:)展示订阅选项,用.productViewStyle和.containerBackground自定义外观
关联 Session
- Meet StoreKit for SwiftUI — StoreKit SwiftUI 视图的完整用法和自定义
- Explore testing in-app purchases — StoreKit 测试的深入指南
- Meet the App Store Server Library — 服务端收据验证和通知处理
评论
GitHub Issues · utterances