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.
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:
.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. 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 {
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? Why does onAppear doesn’t load the IAP? Where should I put the code to make the modal run when the view loaded?
Update 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()
}
}
3
Answers
Your
.onAppear{}
should be Swift code insteadSwiftUI (ForEach, VStack)
. VStack are view structs.I would recommend to separate logic into a viewmodel, and you only need to manage one identified object to show your pro modal.
I just wrote without compiling, please check with your xcode.
Instead of using
.onAppear
modifier to display the modal, you can change the initial values ofselection
andshowModal
:This way, modal view will be shown instantly after the content view loads.
Note: For
showModal
, I’ve applied a conditionalif
instead of simplytrue
, since you said you want to show the modal only to those who have not subscribed yet to the Pro IAP.