[英]How to use if/ForEach in a SwiftUI View to show IAP modal?
I'm trying to display a Subscribe Now modal view instantly after the app starts to encourage users to subscribe to the Pro In-App Purchase, so I used the .onAppear
modifier, and it works fine only if I want to show the modal every time the app starts.我试图在应用程序开始鼓励用户订阅 Pro In-App Purchase 后立即显示一个立即订阅模式视图,所以我使用了
.onAppear
修饰符,它只有在我想显示模式时才能正常工作应用程序启动的时间。
struct ContentView: View {
@State private var selection: String? = nil
@State private var showModal = false
@ObservedObject var storeManager: StoreManager
var body: some View {
NavigationView {
VStack {
// Contents Here
}
}
.onAppear {
self.selection = "Pro"
self.showModal.toggle()
}
.sheet(isPresented: $showModal) {
if self.selection == "Pro" {
Pro(showModal: self.$showModal, storeManager: self.storeManager)
.onAppear(perform: {
SKPaymentQueue.default().add(storeManager)
})
}
}
}
}
Now, the problem begins when I want to display the modal only to those who have not subscribed yet to the Pro IAP, so I modified .onAppear
to:现在,当我只想向尚未订阅 Pro IAP 的人显示模式时,问题就开始了,因此我
.onAppear
修改为:
.onAppear {
ForEach(storeManager.myProducts, id: \.self) { product in
VStack {
if !UserDefaults.standard.bool(forKey: product.productIdentifier) {
self.selection = "Pro"
self.showModal.toggle()
}
}
}
}
But, the if
and ForEach
seems not to work smoothly with structs and views.但是,
if
和ForEach
似乎不能顺利地与结构和视图一起使用。 How should I use them in my case?在我的情况下我应该如何使用它们?
Update:更新:
Based on the answers, I have changed the loop inside .onAppear
to make the code conforms to SwiftUI requirements:根据答案,我更改了
.onAppear
内的循环以使代码符合 SwiftUI 要求:
.onAppear {
storeManager.myProducts.forEach { product in
// Alternatively, I can use (for in) loop:
// for product in storeManager.myProducts {
if !UserDefaults.standard.bool(forKey: product.productIdentifier) {
self.selection = "Pro"
self.showModal.toggle()
}
}
}
Now, errors have gone away but the modal is not displayed on startup.现在,错误已经消失,但启动时不显示模式。
I discovered that the problem is, storeManager.myProducts
is not loaded in .onAppear
modifier, while it's loaded correctly when I put the same loop in a button instead of.onAppear, any ideas?我发现问题是,
storeManager.myProducts
没有加载到.onAppear
修饰符中,而当我将相同的循环放入按钮而不是 .onAppear 时,它会正确加载,有什么想法吗? Why does onAppear doesn't load the IAP?为什么 onAppear 不加载 IAP? Where should I put the code to make the modal run when the view loaded?
当视图加载时,我应该在哪里放置代码以使模式运行?
Update 2:更新 2:
Here is a Minimal Reproducible Example:这是一个最小的可重现示例:
App:应用程序:
import SwiftUI
@main
struct Reprod_SOFApp: App {
@StateObject var storeManager = StoreManager()
let productIDs = ["xxxxxxxxxxxxxxxxxxxxx"]
var body: some Scene {
DocumentGroup(newDocument: Reprod_SOFDocument()) { file in
ContentView(document: file.$document, storeManager: storeManager)
.onAppear() {
storeManager.getProducts(productIDs: productIDs)
}
}
}
}
ContentView:内容视图:
import SwiftUI
import StoreKit
struct ContentView: View {
@Binding var document: Reprod_SOFDocument
@State private var selection: String? = nil
@State private var showModal = false
@ObservedObject var storeManager: StoreManager
var test = ["t"]
var body: some View {
TextEditor(text: $document.text)
.onAppear {
// storeManager.myProducts.forEach(id: \.self) { product in
// Alternatively, I can use (for in) loop:
for i in test {
if !i.isEmpty {
self.selection = "Pro"
self.showModal.toggle()
}
}
}
.sheet(isPresented: $showModal) {
if self.selection == "Pro" {
Modal(showModal: self.$showModal, storeManager: self.storeManager)
.onAppear(perform: {
SKPaymentQueue.default().add(storeManager)
})
}
}
}
}
Modal:模态:
import SwiftUI
import StoreKit
struct Modal: View {
@Binding var showModal: Bool
@ObservedObject var storeManager: StoreManager
var body: some View {
Text("hello world")
}
}
StoreManager:店经理:
import Foundation
import StoreKit
class StoreManager: NSObject, ObservableObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
@Published var myProducts = [SKProduct]()
var request: SKProductsRequest!
@Published var transactionState: SKPaymentTransactionState?
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
transactionState = .purchasing
case .purchased:
UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
queue.finishTransaction(transaction)
transactionState = .purchased
case .restored:
UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
queue.finishTransaction(transaction)
transactionState = .restored
case .failed, .deferred:
print("Payment Queue Error: \(String(describing: transaction.error))")
queue.finishTransaction(transaction)
transactionState = .failed
default:
queue.finishTransaction(transaction)
}
}
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("Did receive response")
if !response.products.isEmpty {
for fetchedProduct in response.products {
DispatchQueue.main.async {
self.myProducts.append(fetchedProduct)
}
}
}
for invalidIdentifier in response.invalidProductIdentifiers {
print("Invalid identifiers found: \(invalidIdentifier)")
}
}
func getProducts(productIDs: [String]) {
print("Start requesting products ...")
let request = SKProductsRequest(productIdentifiers: Set(productIDs))
request.delegate = self
request.start()
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Request did fail: \(error)")
}
func purchaseProduct(product: SKProduct) {
if SKPaymentQueue.canMakePayments() {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
} else {
print("User can't make payment.")
}
}
func restoreProducts() {
print("Restoring products ...")
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
Here is a link to Minimal Reproducible Example 这是指向最小可重现示例的链接
You're trying to use View
code in onAppear
, which doesn't accept a View
-- it should just be regular imperative Swift code.您正在尝试在
onAppear
中使用View
代码,它不接受View
- 它应该只是常规的命令式 Swift 代码。 So, you likely want something like this:所以,你可能想要这样的东西:
.onAppear {
storeManager.myProducts.forEach { product in
if !UserDefaults.standard.bool(forKey: product.productIdentifier) {
self.selection = "Pro"
self.showModal.toggle() //maybe change this to = true -- toggle could *close* it again if there were multiple items that qualified
}
}
}
be careful, on your.onAppear{}, it should be Swift code instead SwiftUI (ForEach, VStack are view structs)小心,在 your.onAppear{} 上,它应该是 Swift 代码而不是 SwiftUI(ForEach,VStack 是视图结构)
I would recommend to separate logic into a viewmodel, and you only need to manage one identified object to show your pro modal.我建议将逻辑分离到一个视图模型中,您只需管理一个已识别的 object 即可显示您的专业模式。
struct ContentView: View {
@ObservedObject var viewModel: ContentViewModel
var body: some View {
Text("Hello")
.onAppear(perform: viewModel.fetchStatus)
.sheet(item: $viewModel.carrier) { carrier in
ModalView(storeManager: carrier.storeManager)
}
}
}
class ContentViewModel: ObservableObject {
@Published var carrier: ModalObject?
private let storeManager: StoreManager
func fetchStatus() {
// do something asynchronous like
storeManager.fetchProducts() { [self] products in
if !products.contains(proProducts) {
self.carrier = ModalObject(storeManager: self.storeManager)
}
}
}
}
struct ModalObject: Identifiable {
var id = UUID()
let storeManager: StoreManager
}
I just wrote without compiling, please check with your xcode.我只是写了没有编译,请检查你的xcode。
Instead of using .onAppear
modifier to display the modal, you can change the initial values of selection
and showModal
:您可以更改
selection
和showModal
的初始值,而不是使用.onAppear
修饰符来显示模式:
@State private var selection: String? = "Pro"
@State private var showModal = !UserDefaults.standard.bool(forKey: "xxxxxxxxxxxxxxxxxxxxx") ? true : false
// Write your product identifier instead of "xxxxxxxxxxxxxxxxxxxxx"
This way, modal view will be shown instantly after the content view loads.这样,模式视图将在内容视图加载后立即显示。
Note: For showModal
, I've applied a conditional if
instead of simply true
, since you said you want to show the modal only to those who have not subscribed yet to the Pro IAP.注意:对于
showModal
,我应用了条件if
而不是简单地true
,因为您说您只想向尚未订阅 Pro IAP 的人显示模式。
One approach is to have a model that keep track of when you ask the user.一种方法是使用 model 来跟踪您何时询问用户。
import SwiftUI
struct AskedModel: Codable{
var id: String
//Date the user was last asked whatever the id represents
var lastAsked: Date?
//Sets the `lastAsked` variable to today
mutating func setAskedToday(){
lastAsked = Date()
}
//Checks if the user was last asked `n` days ago
//The default is 30 days
func askedNDaysAgo(n: Int = 30) -> Bool{
if lastAsked != nil{
return Calendar.current.dateComponents([.day], from: Date(), to: lastAsked!).day ?? 0 >= n
}else{
return false
}
}
//Saves the AskedModel to UserDefaluts
func saveToUserDefaults(){
let defaults = UserDefaults.standard
let encoder = JSONEncoder()
do{
let encoded = try encoder.encode(self)
defaults.set(encoded, forKey: self.id + "-asked")
}catch{
print(error)
}
}
//Gets the AskedModel represented by the id provided from UserDefaults
static func getFromUserDefaults(id: String) -> AskedModel{
var result = AskedModel(id: id)
let defaults = UserDefaults.standard
if let saved = defaults.object(forKey: id + "-asked") as? Data{
let decoder = JSONDecoder()
do{
result = try decoder.decode(AskedModel.self, from: saved)
}catch{
print(error)
}
}
return result
}
}
Then in the view model you can present the sheet based on when the user was last asked and the store manager然后在视图 model 中,您可以根据上次询问用户的时间和商店经理来展示表格
class ProViewModel: ObservableObject{
var productIdentifier: String = "com.yourCompany.Pro"
var proAsked: AskedModel{
get{
AskedModel.getFromUserDefaults(id: productIdentifier)
}
set{
newValue.saveToUserDefaults()
objectWillChange.send()
}
}
@Published var showSheet: Bool = false
init(){
showSheet = shouldAsk()
}
func shouldAsk() -> Bool{
var result = !isPurchased()
print(result)
if result{
result = !proAsked.askedNDaysAgo()
print(result)
}
return result
}
func isPurchased() -> Bool{
//Ask Store if purchased
false
}
}
Then use the showSheet
variable on the sheet
that presents the ProView
and onDismiss
call setAskedToday
so the model will be updated with today's date.然后在显示
ProView
和onDismiss
调用setAskedToday
的工作sheet
上使用showSheet
变量,以便 model 将更新为今天的日期。
struct ConditionalView: View {
@StateObject var proVM: ProViewModel = ProViewModel()
var body: some View {
Text("Hello, World!")
.sheet(isPresented: $proVM.showSheet, onDismiss: {
proVM.proAsked.setAskedToday()
}, content: {
Text("ProView")
})
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.