SwiftUI文档类应用付费墙问题排查及最优实现方案咨询
问题描述
我的应用启动后直接显示Paywall,测试时一切正常,但每次向App Store Connect提交更新时,都会收到反馈:用户购买内购订阅后无法获取高级功能(即付费墙在购买成功后未消失)。
我的用户权限授予方案如下:
- 主应用视图权限检查:在
DocumentView的.onAppear中通过RevenueCat拉取用户信息,判断是否显示Paywall,并把状态存在UserDefaults里
@main struct MyApp: App { var body: some Scene { DocumentView() } } struct DocumentView: Scene { @State var showModal = false var body: some Scene { DocumentGroup(newDocument: Document()) { file in ContentView(document: file.$document, showModal: $showModal) .onAppear() { Purchases.shared.getCustomerInfo { customerInfo, error in if customerInfo?.entitlements[Constant.entitlementId]?.isActive == true { self.showModal = false UserDefaults.standard.setValue(true, forKey: Constant.userDefaultsAccess) } else { self.showModal = true UserDefaults.standard.setValue(false, forKey: Constant.userDefaultsAccess) } } } } } }
- 内容视图Paywall显示逻辑:通过
showModal和selection变量控制不同类型的Paywall显示,按钮图标和状态依赖UserDefaults
struct ContentView: View { @State private var selection: String? = "NoPro" @Binding var showModal: Bool var body: some View { NavigationView { VStack { .... } } .navigationBarItems( trailing: HStack { Button(action: { self.selection = "Pro" self.showModal.toggle() }, label: { Image(systemName: UserDefaults.standard.bool(forKey: Constant.userDefaultsAccess) ? "checkmark.seal.fill" : "xmark.seal.fill") }) .foregroundColor(UserDefaults.standard.bool(forKey: Constant.userDefaultsAccess) ? .green : .red) .sheet(isPresented: $showModal) { if self.selection == "Pro" { Paywall(showModal: self.$showModal, pro: "Pro") } else if self.selection == "NoPro" { Paywall(showModal: self.$showModal, pro: "NoPro") } } } }
- Paywall购买后处理:购买成功后修改UserDefaults并关闭弹窗,判断是否显示订阅状态依赖UserDefaults
struct Paywall: View { @Binding var showModal: Bool @State var pro: String var body: some View { VStack { .... if pro == "Pro" { HStack { Spacer() VStack { Image(systemName: "xmark.circle.fill") .font(.largeTitle) .padding() .onTapGesture(count: 1, perform: { self.showModal.toggle() }) Spacer() } } } SubscriptionButton(showModal: $showModal) } .interactiveDismissDisabled(pro == "Pro" ? false : true) } } struct SubscriptionButton: View { @Binding var showModal: Bool var body: some View { if UserDefaults.standard.bool(forKey: Constant.userDefaultsAccess) { Text("You are already Subscribed!") } else { Button(action: { PurchaseManager.purchase(productId: Constant.productId) { UserDefaults.standard.setValue(true, forKey: Constant.userDefaultsAccess) showModal = false } }, label: { VStack { Text("Subscribe now!") } }) } } }
现有实现的问题
- 依赖本地存储判断权限:把UserDefaults作为权限判断的核心依据,但本地存储无法和RevenueCat服务器实时同步——比如用户在其他设备购买、订阅过期/续订、服务器调整订阅状态时,本地值不会自动更新,直接导致状态判断错误。
- 权限检查时机单一:只在应用启动时拉取一次用户信息,购买成功后仅修改本地值,没有主动重新拉取服务器最新状态,一旦网络延迟或者回调有误差,就会出现状态不同步。
- 视图状态和订阅状态脱节:
showModal、selection等视图状态完全靠本地变量控制,没有和服务器订阅状态绑定,购买成功后手动修改状态可能因为SwiftUI的更新机制导致UI没有及时刷新。 - 购买回调不严谨:购买完成后直接修改本地值并关闭弹窗,没有验证购买是否真正成功(比如没检查RevenueCat返回的
CustomerInfo),如果支付过程出现异常(比如失败但回调误触发),会错误授予权限。
最优实现方案
核心原则:完全以RevenueCat服务器返回的CustomerInfo作为权限判断唯一标准,用可观察对象管理订阅状态,让UI自动响应状态变化
1. 全局订阅状态管理器
创建一个ObservableObject类,统一处理订阅状态的获取、更新,确保所有视图都使用同一数据源:
import RevenueCat class SubscriptionManager: ObservableObject { static let shared = SubscriptionManager() @Published var hasActiveSubscription = false private let entitlementId = Constant.entitlementId init() { fetchCustomerInfo() // 监听购买、恢复订阅的成功回调,自动更新状态 Purchases.shared.purchaseSuccessHandler = { customerInfo in self.updateSubscriptionStatus(with: customerInfo) } Purchases.shared.restoreSuccessHandler = { customerInfo in self.updateSubscriptionStatus(with: customerInfo) } } // 拉取最新用户订阅信息 func fetchCustomerInfo() { Purchases.shared.getCustomerInfo { [weak self] customerInfo, error in guard let self = self, let customerInfo = customerInfo else { return } self.updateSubscriptionStatus(with: customerInfo) } } // 更新订阅状态(必须在主线程更新UI) private func updateSubscriptionStatus(with customerInfo: CustomerInfo) { DispatchQueue.main.async { self.hasActiveSubscription = customerInfo.entitlements[self.entitlementId]?.isActive == true } } }
2. 改造应用入口逻辑
移除UserDefaults依赖,用SubscriptionManager的状态控制Paywall显示:
@main struct MyApp: App { @StateObject private var subscriptionManager = SubscriptionManager.shared var body: some Scene { DocumentView() .environmentObject(subscriptionManager) } } struct DocumentView: Scene { @EnvironmentObject private var subscriptionManager: SubscriptionManager @State var showModal = false var body: some Scene { DocumentGroup(newDocument: Document()) { file in ContentView(document: file.$document, showModal: $showModal) .onAppear { // 每次视图出现时刷新最新状态 subscriptionManager.fetchCustomerInfo() } .onChange(of: subscriptionManager.hasActiveSubscription) { hasActive in // 订阅状态变化时自动控制Paywall显示 showModal = !hasActive } } } }
3. 简化ContentView的Paywall控制
移除本地状态变量,直接用SubscriptionManager的状态区分不同场景的Paywall:
struct ContentView: View { @EnvironmentObject private var subscriptionManager: SubscriptionManager @Binding var showModal: Bool @State private var showProPaywall = false // 标记用户主动打开的Paywall var body: some View { NavigationView { VStack { // 应用主内容 } } .navigationBarItems(trailing: Button(action: { showProPaywall = true }) { Image(systemName: subscriptionManager.hasActiveSubscription ? "checkmark.seal.fill" : "xmark.seal.fill") .foregroundColor(subscriptionManager.hasActiveSubscription ? .green : .red) } ) // 启动时强制显示的Paywall(无关闭按钮) .sheet(isPresented: $showModal) { Paywall(showModal: $showModal, isProVersion: false) } // 用户主动打开的Paywall(带关闭按钮) .sheet(isPresented: $showProPaywall) { Paywall(showModal: $showProPaywall, isProVersion: true) } } }
4. 改造Paywall和购买按钮
彻底移除UserDefaults,购买后自动刷新服务器状态:
struct Paywall: View { @Binding var showModal: Bool let isProVersion: Bool @EnvironmentObject private var subscriptionManager: SubscriptionManager var body: some View { VStack { // Paywall内容 // 仅用户主动打开的Paywall显示关闭按钮 if isProVersion { HStack { Spacer() Image(systemName: "xmark.circle.fill") .font(.largeTitle) .padding() .onTapGesture { showModal = false } } } SubscriptionButton() } .interactiveDismissDisabled(!isProVersion) .onAppear { // 打开Paywall时刷新最新状态 subscriptionManager.fetchCustomerInfo() } } } struct SubscriptionButton: View { @EnvironmentObject private var subscriptionManager: SubscriptionManager var body: some View { if subscriptionManager.hasActiveSubscription { Text("您已订阅") .foregroundColor(.green) } else { Button(action: { PurchaseManager.purchase(productId: Constant.productId) { success in if success { // 购买成功后刷新服务器状态,由管理器自动更新UI SubscriptionManager.shared.fetchCustomerInfo() } } }) { Text("立即订阅") .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) } } } } // 优化PurchaseManager,确保返回真实购买结果 class PurchaseManager { static func purchase(productId: String, completion: @escaping (Bool) -> Void) { Purchases.shared.purchaseProduct(productId) { transaction, customerInfo, error, userCancelled in if let error = error || userCancelled { completion(false) return } // 验证订阅是否真的激活 let hasActive = customerInfo?.entitlements[Constant.entitlementId]?.isActive == true completion(hasActive) } } }
关键改进点
- 单一数据源:所有权限判断都基于RevenueCat服务器数据,彻底抛弃本地存储作为权限依据
- 自动状态同步:通过
@Published属性让SwiftUI视图自动响应订阅状态变化,无需手动控制弹窗显示 - 多时机刷新:视图出现、Paywall打开、购买完成后都主动拉取最新状态,确保和服务器一致
- 严谨的购买验证:购买完成后先验证订阅是否激活,再更新状态,避免错误授权
内容的提问来源于stack exchange,提问作者Adam M.
相关产品推荐
相关产品推荐

