前情提要
因为设计了一款“存钱猪猪”的应用,想要在免费的基础之上,提供一个赞助的内购功能,因此产生了这篇文章。
在处理应用内购代码过程中也遇到了几处问题,在这里分享一下。
(1)首先,我第一次提供内购功能后,没有经过沙盒测试,因为我发现应用内购时会报错,而我考虑过后,所想到的就是内购产品也没有审核通过,所以想着苹果审核人员会在审核通过内购产品后,再对应用的购买进行审核。
结果就因为内购功能提示:Cannot Connect to iTunes Store
![](https://fangjunyu.com/wp-content/uploads/2024/05/1-16.png)
苹果给予的答复是应用出现一个或多个对App Store产生负面影响的错误。以及我的应用界面中存在赞助的具体金额:Support free development ($1 USD),被苹果认为应该从应用中删除任何对定价的引用,如果想要宣传应用价格的变化,需要在应用说明中包含此信息。
关于应用价格的变化,我理解的是在应用内购中展示介绍内容,当然我后面把这段问题删除了。
(2)我第二次提供应用时,将上面的内购展示功能删除了,因为我想应该得先审核通过内购,然后我才能测试,同时修改的赞助文字的部分。
这一次,苹果给我的反馈是审核您的应用,但无法继续,因为无法在应用中找到应用内购。
这两次的反馈总结,我想无法连接iTunes Store,应该不是内购未审核通过问题,因此,我应该进行测试。然后从网络上查找相关的测试教程,接着修改内购的实现代码,在应用沙箱中完成的内购的测试。
(3)第三次提审时,我还特意进行备注:“我已经重新提交内购展示代码,并且已经通过沙盒进行购买测试,当我使用TestFlight时,发现无法打开商品页,查询了一下原因可能为内购商品没有审核通过,如果内购商品没有审核通过,我就不可能使用真正的购买页面,如果我只提供内购商品,没有购买功能,又是这次报错,这成了一个悖论。”
这一次,应用审核通过了。
很高兴,我的应用会上架到Apple Store市场上,虽然需要我等待24小时。
![](https://fangjunyu.com/wp-content/uploads/2024/05/2-14.png)
很高兴我能够分享我的内购代码,希望能给需要的朋友带来帮助。
配置Xcode项目
我们在实现代码前,还需要一些配置:
- 在项目中点击左侧项目名称,找到TARGETS -> Signing & Capabilities -> TARGETS,点击Capability
![](https://fangjunyu.com/wp-content/uploads/2024/05/3-12.png)
输入In-App Purchase,然后双击将功能添加到我们的项目中去。
![](https://fangjunyu.com/wp-content/uploads/2024/05/4-9.png)
另外,我们还需要检查Bundle Identifier是否输入正确。
![](https://fangjunyu.com/wp-content/uploads/2024/05/5-7.png)
上述两个操作确认完成后,我们开始实现内购功能。
实现代码
下面是内购功能实现的具体代码:
1、IAPManager文件
import StoreKit
@available(iOS 15.0, *)
class IAPManager {
static let shared = IAPManager()
private init() {}
func fetchSingleProduct() async throws -> Product? {
let productID = "单个产品id" // 注意,要替换成实际的产品ID
print("Fetching product with ID: \(productID)")
let products = try await Product.products(for: [productID])
if let product = products.first {
print("Product fetched: \(product)")
} else {
print("Product not found")
}
return products.first
}
func purchaseProduct(_ product: Product) async throws -> Product.PurchaseResult {
print("Purchasing product: \(product.id)")
let result = try await product.purchase()
switch result {
case .success(let verification):
// 验证交易
print("Purchase successful, verifying transaction")
let transaction = try checkVerified(verification)
// 更新购买状态
savePurchasedState(for: product.id)
await transaction.finish()
return result
case .userCancelled:
print("Purchase cancelled by user")
return result
case .pending:
print("Purchase pending")
return result
default:
print("Purchase failed with result: \(result)")
throw StoreError.failedVerification
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified:
print("Failed verification")
throw StoreError.failedVerification
case .verified(let signedType):
return signedType
}
}
func handleTransactions() async {
for await result in Transaction.updates {
do {
let transaction = try checkVerified(result)
// 处理交易,例如解锁内容
savePurchasedState(for: transaction.productID)
await transaction.finish()
} catch {
print("交易处理失败:\(error)")
}
}
}
func savePurchasedState(for productID: String) {
// 保存购买状态到用户偏好设置或其他存储位置
UserDefaults.standard.set(true, forKey: productID)
print("Purchased state saved for product: \(productID)")
}
func loadPurchasedState(for productID: String) -> Bool {
let isPurchased = UserDefaults.standard.bool(forKey: productID)
print("Purchased state loaded for product: \(productID) - \(isPurchased)")
return isPurchased
}
}
enum StoreError: Error {
case failedVerification
}
2、View文件
import SwiftUI
import StoreKit
@available(iOS 15.0, *)
struct View: View {
@State private var product: Product?
@State private var purchaseResult: Product.PurchaseResult?
@State private var isPurchased: Bool = false
var body: some View {
VStack(alignment: .leading) {
if isPurchased {
Text(SupportedThisApplication)
} else if let product = product {
Text(SupportApp)
} else {
Text("加载中...")
}
}
.onAppear {
Task {
await loadProduct()
checkPurchaseStatus()
}
}
}
private func loadProduct() async {
do {
print("加载产品中...")
if let singleProduct = try await IAPManager.shared.fetchSingleProduct() {
product = singleProduct
print("产品加载成功: \(singleProduct)")
} else {
print("未找到产品")
}
} catch {
print("加载产品失败:\(error)")
}
}
private func resetProduct() async {
product = nil
await loadProduct()
}
private func checkPurchaseStatus() {
let productID = "单个产品ID" // 注意,要替换成实际的产品ID
isPurchased = IAPManager.shared.loadPurchasedState(for: productID)
}
}
这里的代码是支持单一内购商品的,注意代码中的单个产品ID需要替换为实际的内购产品ID,在App Store Conntent中查看内购产品ID:
![](https://fangjunyu.com/wp-content/uploads/2024/05/6-4.png)
Xcode项目的具体代码,可以访问 piggyBank 的 Github进行查看:https://github.com/fangjunyu1/piglet
完成代码后,我们需要进行沙盒测试,以测试内购应用的完整性。
沙盒测试
我们完成内购代码后,在测试手机上找到设置-App Store-沙盒账户-登录沙盒账户的Apple ID,
![](https://fangjunyu.com/wp-content/uploads/2024/05/7-4.png)
我不知道Xcode模拟器是否可以登录,我是使用的真机进行沙盒测试的。
然后,我们使用Xcode将应用安装到iPhone上,点击实现的内购按钮。
![](https://fangjunyu.com/wp-content/uploads/2024/05/8-4.png)
最后,页面调出我们心心念念的内购视图,完成沙盒内购。
但存在一个问题,就是我发现首次内购完成后,Xcode会提示网络错误,然后首次内购完成,会再次弹出内购功能。
我虽然在代码中添加了内购校验,但因为我的内购项目属于非消耗型内购,所以再次购买或者取消,都不会额外付费,而且重新进入应用后,会校验内购并展示我的内购完成视图。
当你的代码是非消耗性内购时,使用这个代码,会存在让用户误解的情况,同时,因为校验没有走服务器,也跟实际公司的生产环境不符。因此这里实现内购的环境比较单一,不具备普适性,只能用作个人应用的非消耗性内购代码参考。
如果想要重置内购,可以到刚才设置沙盒账号的页面,点击沙盒账号-管理-清除购买历史记录。
![](https://fangjunyu.com/wp-content/uploads/2024/05/9-3.png)
以上是本次iOS通过StoreKit 2实现内购的全部内容,我的“存钱猪猪”应用在本文章发布的24小时后,会上架到全部的Apple Store,希望各位游客可以去下载并尝试使用。
我现在还在学习Sketch,因为我发现设计应用的主要思路还是需要一个参考页面,才能分析并实现对应的功能,之前我设想的是直接用Xcode写页面,现在看来也是盲人摸象,很难写出好的应用。
最后,希望看到这篇文章的人,也能够实现内购功能,完成个人的内购应用,异或实现公司交与的任务需求。
还要感谢一下苹果的审核人员,虽然退审了两次,但是审核时间还都是蛮快的,可能因为我的应用简单,基本上是一天或者半天就告知我审核结果,而且审核人员可能是全球性质的,我记得使用Shynet查看发现有德国Apple ip,美国Apple ip,这也许是苹果审核比较快的原因吧。
完结撒花🎉🎉🎉
附录
原始的 StoreKit API 和最新的 StoreKit 2 API
我们在本次实现内购的代码中,使用的是StoreKit 2 API,因此对支持的系统版本是具有局限性的,只支持iOS 15以上版本。在这里,对两个API进行了简单的介绍和区分。
在 iOS 开发中,App 内购买(In-App Purchase, IAP)是一个非常重要的功能,允许开发者向用户提供额外的内容和服务。对于实现这一功能,Apple 提供了两个主要的 API:原始的 StoreKit API 和最新的 StoreKit 2 API。以下是这两个 API 的主要区别:
StoreKit(原始 API)
- 引入时间:自 iOS 3.0 以来,StoreKit API 一直存在。
- 使用方式:基于委托(delegate)模式和通知(notification)模式。
- 代码复杂性:需要处理大量的样板代码(boilerplate code),如请求和处理交易、管理产品信息、处理交易状态等。
- 并发支持:依赖 GCD(Grand Central Dispatch)或其他传统并发处理方法。
- 支持系统版本:适用于 iOS 3.0 及以后版本,适合支持早期操作系统的应用。
StoreKit 2(最新 API)
- 引入时间:随着 iOS 15 的发布而引入。
- 使用方式:基于 Swift 的现代编程接口,利用 Swift 并发(Swift Concurrency)功能,如 async/await 和结构化并发。
- 代码简化:简化了许多操作,如获取产品信息、进行交易、管理订阅等,使代码更加简洁和易于维护。
- 并发支持:内置对 Swift 并发的支持,允许在异步操作期间内嵌返回结果,避免使用委托对象。
- 支持系统版本:主要针对 iOS 15 及以上版本。
选择使用哪一个 API
- 新应用或现有应用的更新:如果正在开发一个新应用或更新一个现有应用,并且目标操作系统版本是 iOS 15 及以上,建议使用 StoreKit 2。它能简化开发流程,提供更好的并发支持。
- 需要支持早期操作系统的应用:如果应用需要支持 iOS 15 以下的版本,那么需要使用原始的 StoreKit API,或者同时使用两者来兼容不同版本的系统。