skip to Main Content

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()
    }
                      
}

Here is a link to Minimal Reproducible Example

3

Answers


  1. Your .onAppear{} should be Swift code instead SwiftUI (ForEach, VStack). VStack are view structs.

    Login or Signup to reply.
  2. I would recommend to separate logic into a viewmodel, and you only need to manage one identified object to show your pro modal.

    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.

    Login or Signup to reply.
  3. Instead of using .onAppear modifier to display the modal, you can change the initial values of selection and showModal:

    @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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search