WWDC Quick Look 💓 By SwiftGGTeam
What's new in StoreKit and In-App Purchase

What's new in StoreKit and In-App Purchase

观看原视频

Highlight

StoreKit 交易历史 API 终于覆盖已完成的消耗品交易,Transaction 和 RenewalInfo 新增 currency / price 字段,Original API for In-App Purchase 在 iOS 18 被正式标记为 deprecated。


核心内容

开发内购功能时,消耗品购买记录一直是件麻烦事。用户买了 100 金币、消费完毕、交易被 finish 之后,这条记录就从交易历史里消失了。如果你需要展示”购买历史”或做消耗品防重复校验,就得自己建一套数据库来追踪。iOS 18 终于补上了这个缺口:交易历史 API 现在包含已完成的消耗品交易,只需在 Info.plist 里把 SKIncludeConsumableInAppPurchaseHistory 设为 true,框架就会把所有消耗品交易——不管是否已 finish——都纳入查询范围。

另一个长期痛点是价格展示。你能在 App Store Connect 里给产品定价,但 App 内一直拿不到具体的金额和货币信息,只能靠服务端中转或硬编码。iOS 18 给 Transaction 加了 currencyprice,给 RenewalInfo 加了 currencyrenewalPrice。这些字段通过 @backDeployed 向下兼容到 iOS 15,老系统上也能用。

UI 层面,去年 WWDC23 引入的 StoreKit Views 今年大幅扩展。新增了 SubscriptionOptionGroup 用来按服务等级分组展示订阅选项,新增 compactPicker / pagedPicker / pagedProminentPicker 三种控制样式,新增 placement API 控制购买按钮的位置,还开放了 SubscriptionStoreControlStyle 协议让你写完全自定义的订阅商店控件。Win-back offers 是今年新增的订阅优惠类型,专门面向流失用户,而且有机会被 Apple 编辑团队在 App Store 的 Today / Games / Apps 标签页推荐展示。


详细内容

已完成消耗品交易进入历史 API(00:40

此前 Transaction.currentEntitlementsTransaction.all 只返回订阅、非消耗品和未完成消耗品的交易。iOS 18 起,在 Info.plist 设置:

<key>SKIncludeConsumableInAppPurchaseHistory</key>
<true/>

之后,正常的 Transaction.updates 监听和 Transaction.all 查询就会包含已完成的消耗品交易,无需额外代码。

Transaction 和 RenewalInfo 新字段(01:32

  • Transaction.currency:购买时使用的货币代码(如 "USD"
  • Transaction.price:App Store Connect 中配置的价格
  • RenewalInfo.currency:续订时的货币代码
  • RenewalInfo.renewalPrice:下次续订将被收取的金额

这些字段通过 Swift 的 @backDeployed 属性,在 Xcode 16 编译时可用,运行时最低支持 iOS 15。

SubscriptionOptionGroup 与 compactPicker 样式(04:26

官方给出的 Destination Video 订阅商店示例代码:

import StoreKit
import SwiftUI

struct DestinationVideoShop: View {

  var body: some View {
    SubscriptionStoreView(groupID: Self.subscriptionGroupID) {
      SubscriptionOptionGroupSet { product in
        StreamingPassLevel(product)
      } label: { streamingPassLevel in
        Text(streamingPassLevel.localizedTitle)
      } marketingContent: { streamingPassLevel in
        StreamingPassMarketingContent(level: streamingPassLevel)
        StreamingPassFeatures(level: streamingPassLevel)
      }
    }
    .subscriptionStoreControlStyle(.compactPicker, placement: .bottomBar)
  }

}

关键点:

  • SubscriptionOptionGroupSet 根据闭包返回的枚举值(这里是 StreamingPassLevel)自动分组——每个唯一值生成一个分组
  • label 闭包提供分组标题(如”Premium”/“Basic”),框架自动渲染为 tab 切换
  • marketingContent 闭包按当前激活的分组返回不同的营销视图
  • .compactPicker 样式把订阅选项压缩为一行横向选择器,placement: .bottomBar 把控件固定在底部栏,滚动营销内容时始终可见

分组样式切换(09:06

除了默认的 .tabs 样式,还可以用 .links 样式:

.subscriptionStoreOptionGroupStyle(.tabs)   // tab 切换,适合不同服务等级
.subscriptionStoreOptionGroupStyle(.links)   // 导航链接,适合更多选项时用 sheet 展开

自定义控制样式(13:41

iOS 18 开放了 SubscriptionStoreControlStyle 协议,配合 SubscriptionPickerSubscribeButton 原语,你可以从头构建自己的控件:

import StoreKit
import SwiftUI

struct BadgedPickerControlStyle: SubscriptionStoreControlStyle {
  func makeBody(configuration: Configuration) -> some View {
    SubscriptionPicker(configuration) { pickerOption in
      HStack(alignment: .top) {
        VStack(alignment: .leading) {
          Text(pickerOption.displayName)
            .font(title2.bold())
          Text(priceDisplay(for: pickerOption))
          if pickerOption.isFamilyShareable {
            FamilyShareableBadge()
          }
          Text(pickerOption.description)
        }
        Spacer()
        SelectionIndicator(pickerOption.isSelected)
      }
    } confirmation: { option in
      SubscribeButton(option)
    }
  }
}

struct DestinationVideoShop: View {

  var body: some View {
    SubscriptionStoreView(groupID: Self.subscriptionGroupID) {
      SubscriptionPeriodGroupSet { _ in
        StreamingPassMarketingContent()
      }
    }
    .subscriptionStoreControlStyle(BadgedPickerControlStyle())
  }

}

关键点:

  • 遵循 SubscriptionStoreControlStyle 协议,只需实现 makeBody 一个方法
  • configuration 包含构建控件所需的全部数据
  • SubscriptionPicker 是框架提供的原语,负责管理选中状态
  • pickerOption.displayName / .isFamilyShareable / .description 等属性直接从产品元数据读取
  • SubscribeButton(option) 是框架提供的购买按钮原语,点击即触发购买流程
  • SubscriptionPeriodGroupSet 是按订阅周期分组的便捷 API

Win-back offers(02:34

Win-back offers 面向已取消订阅的流失用户。你可以在 App Store Connect 中配置资格规则,无需在 App 里写硬编码判断。展示方式有两种:通过 StoreKit Message API 在 App 内弹出(不需要额外代码),或由 Apple 编辑团队在 App Store 的 Today / Games / Apps 标签页推荐展示。

Xcode 测试增强(15:38

  • StoreKit configuration 新增 App Policies 区域,可本地测试隐私政策和服务条款
  • 支持订阅组显示名称的本地化测试
  • 可测试内购图片(通过 ProductViewprefersPromotionalIcon
  • Dialogs 设置可关闭系统弹窗,方便 UI 自动化测试
  • Transaction Manager 支持发送 Purchase Intent,测试 App Store 推广购买
  • 支持测试 billing issue 消息,模拟订阅续订失败场景

Original API for In-App Purchase 被废弃(21:06

iOS 18 起,SKPaymentQueue 等原始 API 被标记为 deprecated,统一收据(unified receipt)同样废弃。已有 App 不受影响可继续运行,但这些 API 不会再获得新功能。StoreKit 2 是唯一会持续更新的路径。


核心启发

  • 做什么:用 currencyprice 字段替代服务端价格查询。为什么值得做:减少一次网络请求,价格展示更实时,用户切换地区时也不用等服务端同步。怎么开始:在展示价格的 SwiftUI 视图中,从 Transaction 读取 currencyprice,用 NumberFormatter 格式化显示;续订价格同理从 RenewalInfo.renewalPrice 取值。

  • 做什么:用 SubscriptionOptionGroupSet 重构订阅商店 UI。为什么值得做:用声明式代码替代手写的分组逻辑,tab 切换和营销内容联动由框架处理,代码量大幅减少。怎么开始:定义一个枚举表示服务等级(如 premium / basic),在 SubscriptionOptionGroupSet 的闭包中返回该枚举值,框架自动按值分组并渲染 tab 或 links 样式。

  • 做什么:开启 SKIncludeConsumableInAppPurchaseHistory,删除自建的消耗品购买记录逻辑。为什么值得做:省去维护一套本地数据库的成本,Transaction.all 直接返回完整消耗品历史,退款和防重复校验逻辑更简单。怎么开始:在 Info.plist 中添加该 key 并设为 true,然后用 Transaction.updates 监听即可收到已完成消耗品的交易。

  • 做什么:为流失用户接入 win-back offers。为什么值得做:Apple 会在 App Store 编辑推荐位展示你的优惠,获取免费流量;资格规则在 App Store Connect 配置,无需发版更新。怎么开始:在 App Store Connect 中创建 win-back offer 并设置资格条件,通过 StoreKit Message API 在 App 内展示,或在服务端通过 App Store Server API 发放。


关联 Session

评论

GitHub Issues · utterances