Home > Mobile >  A View.environmentObject(_:) may be missing as an ancestor of this view - but not always…
A View.environmentObject(_:) may be missing as an ancestor of this view - but not always…

Time:12-12

I'm getting this error in production and can't find a way to reproduce it.

Fatal error > No ObservableObject of type PurchaseManager found. A View.environmentObject(_:) for PurchaseManager may be missing as an ancestor of this view. > PurchaseManager > SwiftUI

The crash comes from this view:

struct PaywallView: View {
    @EnvironmentObject private var purchaseManager: PurchaseManager
    var body: some View {
        // Call to purchaseManager causing the crash
    }
}

And this view is instantiated in subviews of the MainView

@main
struct MyApp: App {
    let purchasesManager = PurchaseManager.shared
    var body: some Scene {
        WindowGroup {
            MainView()
                .environmentObject(purchasesManager)
            }
        }
    }
}

or, when called from a UIKit controller, from this controler:

final class PaywallHostingController: UIHostingController<AnyView> {
    init() {
        super.init(rootView:
                    AnyView(
                        PaywallView()
                            .environmentObject(PurchaseManager.shared)
                    )
        )
    }
    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

I tested all the use cases that trigger the PaywallView to show up, and I never got a crash.

FWIW, the PurchaseManager looks like this:

public class PurchaseManager: ObservableObject {   
    static let shared       = PurchaseManager()
    init() {
        setupRevenueCat()
        fetchOfferings()
        refreshPurchaserInfo()
    }
}

Why would the ObservableObject go missing? In which circumstances?

CodePudding user response:

Try not to use the singleton pattern here (.shared), EnvironmentObject is meant to be a replacement for it. You should instantiate PurchasesManager in MyApp.

@main
struct MyApp: App {
    @StateObject var purchasesManager = PurchaseManager() 
    var body: some Scene {
        WindowGroup {
            MainView()
                .environmentObject(purchasesManager)
            }
        }
    }
}

without state object compiles fine but needed if you want child views to update automatically.

Doing those things with a dummy PurchasesManager runs fine for me.

CodePudding user response:

The reason your problem is intermittent, is probably because the PurchaseManager init() could finish before all the data is setup properly, due to the "delays" of the async functions in init(). So sometimes the data will be available when the View wants it, and sometimes it will not be there and crash your app.

You could try the following approach that includes @atultw advice of using StateObject.

import SwiftUI

@main
struct TestApp: App {
    @StateObject var purchaseManager = PurchaseManager() // <-- here
    
    var body: some Scene {
        WindowGroup {
            MainView()
                .onAppear {
                    purchaseManager.startMeUp()    // <-- here
                }
                .environmentObject(purchaseManager)
        }
    }
}

struct MainView: View {
    @EnvironmentObject var purchaseManager: PurchaseManager
    
    var body: some View {
        Text("testing")
        List {
            ForEach(purchaseManager.offerings, id: \.self) { offer in
                Text(offer)
            }
        }
    }
}

public class PurchaseManager: ObservableObject {
    @Published var offerings: [String] = []
    
    // -- here --
    func startMeUp() {
        // setupRevenueCat()
        fetchOfferings()
        // refreshPurchaserInfo()
    }
    
    func fetchOfferings() {
        DispatchQueue.main.asyncAfter(deadline: .now() 2) {
            self.offerings = ["offer 1","offer 2","offer 3","offer 4"]
        }
    }
}
  • Related