StoreKit 和应用内购买的新变化
What's new in StoreKit and In-App Purchase
2024年6月10日
一句话判断
iOS 18 终于把已完成的消耗品交易纳入交易历史 API,同时新增货币和价格字段、win-back offers 和更强大的 StoreKit Views,这三个改动组合起来让内购管理从”头疼”变成了”基本不用操心”。
这场 Session 讲了什么
StoreKit 在 iOS 18 带来三个方向的重要更新。第一,交易历史 API 的覆盖范围终于补全了——已完成的消耗品交易(finished consumable transactions)现在可以被查询到,不再需要开发者自己维护一套消耗品购买记录。第二,Transaction 和 RenewalInfo 两个数据模型新增了 currency、price、renewalPrice 字段,让你终于可以在 App 内准确展示价格信息。第三,StoreKit Views(去年 WWDC23 引入的内购 UI 组件)增加了 SubscriptionOptionGroup、compact picker 样式等定制能力,让你用更少的代码构建更专业的订阅商店界面。
另外还有 win-back offers——一种新的订阅优惠类型,专门面向流失用户,可以在 App Store 的 Today、Games、Apps 标签页中被 Apple 编辑团队推荐展示。
值得深挖的点
已完成消耗品进入交易历史:消除一个长期的数据盲区
在 iOS 18 之前,消耗品(consumable)一旦调用 finish() 完成交易,就从交易历史中消失了。这意味着如果你需要一个”购买历史”页面,消耗品的部分必须自己用数据库或文件来记录。更麻烦的是,如果用户换设备或重装 App,未消耗的消耗品可以通过 transaction listener 恢复,但已完成的就彻底找不回来了。
iOS 18 通过一个新的 Info.plist key SKIncludeConsumableInAppPurchaseHistory 开启这个能力。它是 opt-in 的——你需要主动设置才会生效,不会影响已有行为。开启后,通过 Transaction.updates 和 transaction history API 都能收到已完成的消耗品交易。
为什么是 opt-in 而不是默认开启?因为消耗品的交易量可能非常大(想象一个游戏里的金币购买),默认全部暴露可能带来性能和存储方面的意外。Apple 选择让开发者自己评估是否需要这个功能,是合理的保守策略。
SubscriptionOptionGroup:让 StoreKit Views 真正能用
去年推出的 SubscriptionStoreView 一行代码就能展示订阅选项,但定制能力很弱——如果你的订阅有多个层级(比如”基础版”和”高级版”),UI 上只能平铺展示。iOS 18 的 SubscriptionOptionGroup 让你可以按条件分组,框架自动生成带标签页的 UI。
更实用的是 SubscriptionOptionGroup.Set——你只需要提供一个从产品到分组键的映射闭包,框架就自动为每个唯一值创建一个分组。配合 compact picker 样式,可以在极小的空间内展示复杂的订阅选项组合。
这个 API 的设计思路是”声明式分组,自动布局”。你只管描述”这些产品属于高级组、那些属于基础组”,UI 的标签页切换、选中状态、营销内容的动态切换,框架全包了。这比去年”要么全用默认 UI,要么自己从头写”的处境好了太多。
代码片段
开启已完成消耗品交易历史
场景:在 Info.plist 中配置,让交易历史 API 包含已完成的消耗品。
<!-- Info.plist -->
<key>SKIncludeConsumableInAppPurchaseHistory</key>
<true/>
坑:这只是让 transaction history 能查到已完成消耗品,不影响 finish() 的调用时机——消耗品被消费后仍然要尽快 finish()。
按服务层级分组展示订阅选项
场景:流媒体 App 有”高级”和”基础”两个层级,每个层级有月付和年付选项。
SubscriptionStoreView(groupID: "streaming_pass") {
// 高级组
SubscriptionOptionGroup("Premium") { subscription in
subscription.productLevel == .premium
} content: {
PremiumMarketingView()
}
// 基础组
SubscriptionOptionGroup("Basic") { subscription in
subscription.productLevel == .basic
} content: {
BasicMarketingView()
}
}
.subscriptionStoreControlStyle(.compactPicker)
坑:productLevel 是你在 App Store Connect 里设置的自定义属性,需要在代码里定义对应的 enum 来匹配。
获取交易货币和价格
场景:在购买确认页面展示用户实际支付的价格和货币。
for await transaction in Transaction.updates {
if case .verified(let t) = transaction {
// 新增的 currency 和 price 字段
let price = t.price // Decimal
let currency = t.currency // String (ISO 4217)
print("支付: \(price) \(currency ?? "")")
}
}
坑:price 是你在 App Store Connect 配置的价格,不含地区税收调整。实际扣款金额可能不同。这些字段向后兼容到 iOS 15。
最佳实践
新项目:直接上 StoreKit 2 API。把 SKIncludeConsumableInAppPurchaseHistory 默认开启,省去自己维护消耗品购买记录的麻烦。订阅商店 UI 用 SubscriptionStoreView + SubscriptionOptionGroup 搭建,配合 compact picker 样式,用最少代码拿到最专业的效果。
已有项目:如果你还在用 StoreKit 1 的 SKPaymentQueue,强烈建议开始迁移到 StoreKit 2。currency 和 price 字段只有在 StoreKit 2 的 Transaction 模型上才有,SKPaymentTransaction 里没有。新增的消耗品交易历史也只在 StoreKit 2 的 transaction API 上暴露。迁移路径虽然需要改代码,但换来的是大量样板代码的删除。
还有什么值得关注
- Win-back offers 可以在 App Store 的 Today、Games、Apps 标签页被编辑团队推荐展示,是一种新的获客渠道。
RenewalInfo新增了renewalPrice和currency字段,让你可以在订阅管理页面展示”下次续费价格”。- StoreKit Views 新增了自定义按钮样式和头部/底部 slot,可以插入”恢复购买”和”使用条款”链接。