SwiftUI集成Stripe支付时@EnvironmentObject运行时报错问题
运行SwiftUI集成Stripe支付的代码时,触发如下运行时错误:
Thread 16: Fatal error: No ObservableObject of type Order found. A View.environmentObject(_:) for Order may be missing as an ancestor of this view.
报错位置在StripePaymentHandler类的configuration.defaultBillingDetails.email = order.email代码行。SecondView通过表单收集包含邮箱在内的订单信息,需要将订单信息传递给StripePaymentHandler以创建Stripe PaymentIntent。
相关代码
StripePaymentHandler类
import StripePaymentSheet import SwiftUI class StripePaymentHandler: ObservableObject { @EnvironmentObject var order: Order @Published var paymentSheet: PaymentSheet? @Published var showingAlert: Bool = false private let backendtUrl = URL(string: "http://xxxxxxxxx")! private var configuration = PaymentSheet.Configuration() private var clientSecret = "" var paymentIntentID: String = "" var alertText: String = "" var paymentAmount: Int = 0 func preparePaymentSheet() { let url = backendtUrl.appendingPathComponent("prepare-payment-sheet") var request = URLRequest(url: url) request.httpMethod = "POST" let task = URLSession.shared.dataTask(with: request, completionHandler: { [weak self] (data, response, error) in guard let data = data, let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any], let customerId = json["customer"] as? String, let customerEphemeralKeySecret = json["ephemeralKey"] as? String, let clientSecret = json["clientSecret"] as? String, let paymentIntentID = json["paymentIntentID"] as? String, let publishableKey = json["publishableKey"] as? String, let self = self else { return } self.clientSecret = clientSecret self.paymentIntentID = paymentIntentID print("I got the paymentIntentID from prepare-payment-sheet = ", self.paymentIntentID) STPAPIClient.shared.publishableKey = publishableKey configuration.merchantDisplayName = "Gee-Ware" configuration.customer = .init(id: customerId, ephemeralKeySecret: customerEphemeralKeySecret) configuration.defaultBillingDetails.address.country = "US" configuration.defaultBillingDetails.email = order.email configuration.billingDetailsCollectionConfiguration.attachDefaultsToPaymentMethod = true configuration.allowsDelayedPaymentMethods = true configuration.primaryButtonColor = UIColor(red: 0.70, green: 0.54, blue: 0.93, alpha: 1.0) configuration.applePay = .init( merchantId: "merchant.com.your_app_name", merchantCountryCode: "US" ) configuration.returnURL = "your-app://stripe-redirect" }) task.resume() } }
Order类
import Foundation class Order: ObservableObject { @Published var name: String = "" @Published var email: String = "" @Published var streetAddress: String = "" @Published var city: String = "" @Published var state: String = "" @Published var zip: String = "" }
SecondView结构体
import SwiftUI import StripePaymentSheet struct SecondView: View { @EnvironmentObject var order: Order @State private var isLocked = true @State private var isLoading = false let layoutProperties:LayoutProperties @State var readyForPayment: Bool = false @FocusState var textFieldFocused: Bool @StateObject var model = StripePaymentHandler() @EnvironmentObject var gloVars: GlobalVars @State private var enteredNumber = "" var enteredNumberFormatted: Double { return (Double(enteredNumber) ?? 0) / 100 } var body: some View { ResponsiveView { properties in VStack { Text("Slide to start payment process, hit button to confirm payment amount, then enter your payment details and your book is on its way to you") Form { Section { TextField("Name", text: $order.name) TextField("Email Address", text: $order.email) } Section { TextField("Street Address", text: $order.streetAddress) TextField("City", text: $order.city) TextField("State", text: $order.state) TextField("Zip", text: $order.zip) } } GeometryReader { geometry in ZStack(alignment: .leading) { BackgroundComponent() DraggingComponent(isLocked: $isLocked, isLoading: isLoading, maxWidth: geometry.size.width) } } .frame(height: 50) .padding() .padding(.bottom, 20) .onChange(of: isLocked) { isLocked in guard !isLocked else { return } readyForPayment = true } Spacer() if let paymentSheet = model.paymentSheet { PaymentSheet.PaymentButton( paymentSheet: paymentSheet, onCompletion: model.onPaymentCompletion ) { payButton } } } } .alert(model.alertText, isPresented: $model.showingAlert) { Button("OK", role: .cancel) { } } .onChange(of: readyForPayment) { DispatchQueue.global(qos: .background).sync { model.updatePaymentSheet() } } .onAppear { let priceAfterProfit = gloVars.bkAmount*100*gloVars.profitFactor enteredNumber = "\(priceAfterProfit)" model.paymentAmount = Int(enteredNumberFormatted * 100) model.preparePaymentSheet() textFieldFocused = true } .toolbar { ToolbarItem(placement: .keyboard) { Button("Done") { textFieldFocused = false } } } } }
问题原因
@EnvironmentObject仅能在View结构体或被View通过环境注入的ObservableObject中使用,直接在自定义的ObservableObject类(如StripePaymentHandler)里声明@EnvironmentObject var order: Order是无效的——环境对象的注入依赖View层级,普通ObservableObject不会自动获取环境中的对象。
解决方法
方法1:初始化时传入Order实例
修改StripePaymentHandler,移除@EnvironmentObject,改为初始化时接收Order对象:
class StripePaymentHandler: ObservableObject { let order: Order @Published var paymentSheet: PaymentSheet? @Published var showingAlert: Bool = false private let backendtUrl = URL(string: "http://xxxxxxxxx")! private var configuration = PaymentSheet.Configuration() private var clientSecret = "" var paymentIntentID: String = "" var alertText: String = "" var paymentAmount: Int = 0 init(order: Order) { self.order = order } func preparePaymentSheet() { // 原有网络请求代码不变 configuration.defaultBillingDetails.email = self.order.email // 其他代码不变 } }
在SecondView中创建StripePaymentHandler时传入已有的order环境对象:
struct SecondView: View { @EnvironmentObject var order: Order @StateObject var model: StripePaymentHandler init(layoutProperties: LayoutProperties) { self.layoutProperties = layoutProperties self._model = StateObject(wrappedValue: StripePaymentHandler(order: Order())) } // 原有代码不变 }
方法2:调用方法时传递Order信息
修改StripePaymentHandler的preparePaymentSheet方法,添加Order参数:
func preparePaymentSheet(with order: Order) { // 原有网络请求代码不变 configuration.defaultBillingDetails.email = order.email // 其他代码不变 }
在SecondView的onAppear中调用时传入order:
.onAppear { let priceAfterProfit = gloVars.bkAmount*100*gloVars.profitFactor enteredNumber = "\(priceAfterProfit)" model.paymentAmount = Int(enteredNumberFormatted * 100) model.preparePaymentSheet(with: order) textFieldFocused = true }
内容的提问来源于stack exchange,提问作者user1233894

