Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No ObservableObject of type NavigationStack found. A View.environmentObject(_:) for NavigationStack may be missing as an ancestor of this view. #35

Closed
optimalpursuits opened this issue Dec 14, 2020 · 4 comments

Comments

@optimalpursuits
Copy link

optimalpursuits commented Dec 14, 2020

Fantastic library.

What I did:

  • I created a NavigationViewModel that will handle navigation across pages that need it. See NavigationVM below.
  • Programmatically want to push to the next view while providing the viewId

Error:
No ObservableObject of type NavigationStack found. A View.environmentObject(_:) for NavigationStack may be missing as an ancestor of this view.

Thank you for your help.

import Foundation
import SwiftUI
import NavigationStack

class NavigationVM: ObservableObject {
    @EnvironmentObject private var navigationStack: NavigationStack
    
    init(){
        
    }
    
    func pushView(view: AnyView, viewId: String? = nil){
        DispatchQueue.main.async {
            if viewId?.isEmpty == true {
                self.navigationStack.push(view)
            } else {
                self.navigationStack.push(view, withId: viewId!)
            }
        }
    }
    
    func popView(viewId: String? = nil){
        DispatchQueue.main.async {
            if viewId?.isEmpty == true {
                self.navigationStack.pop()
            } else {
                self.navigationStack.pop(to: PopDestination.view(withId: viewId!))
            }
        }
    }
    
    func popToRoot() {
        DispatchQueue.main.async {
            self.navigationStack.pop(to: .root)
        }
    }
}

Here's my implementation:

var body: some View {
        NavigationStackView {
              merchantGroupsBody()
        }
}

private func merchantGroupsBody() -> some View {
        VStack{
            ForEach(exploreVM.merchantGroups) { merchantGroup in
                if merchantGroup.merchants.count > 0 {
                    VStack (alignment: .leading){
                        merchantGroupHeaderBody(merchantGroup)
                        ScrollView(.horizontal){
                            HStack{
                                ForEach(merchantGroup.merchants, id: \.self){ merchant in
                                    merchantBody(merchant)
                                }
                            }
                        }
                    }
                    //.foregroundColor(.white)
                }
            }
        }
    }

private func merchantBody(_ merchant: Merchant) -> some View {
        var alreadyCached: Bool {
            ImageCache.default.isCached(forKey: merchant.locationImageUrl)
        }
        
        return
            //NavigationLink(destination: MerchantView(MerchantVM(merchant))) {
            VStack (alignment: .leading) {
                KFImage(URL(string: merchant.attachments.first!.url))
                    .onSuccess { r in
                        print("Success: \(merchant.name) - \(r.cacheType)")
                    }
                    .onFailure { e in
                        print("Error for mechant: \(merchant.name): \(e)")
                    }
                    .onProgress { downloaded, total in
                        print("\(downloaded) / \(total))")
                    }
                    .placeholder {
                        HStack {
                            Image(systemName: "arrow.2.circlepath.circle")
                                .resizable()
                                .frame(width: 50, height: 50)
                                .padding(10)
                            Text("Loading...").font(.title)
                        }
                        .foregroundColor(.gray)
                    }
                    .cancelOnDisappear(true)
                    .resizable()
                    .frame(width: 200, height: 100)
                    .aspectRatio(contentMode: .fill)
                    .opacity(doneLoadingImage || alreadyCached ? 1.0 : 0.3)
                    .animation(.linear(duration: 0.4))
                Text(merchant.name)
                Text("\(merchant.distanceToCustomerString) | \(merchant.hoursOfOperationString)")
                    .font(.system(size:12))
                Spacer()
            }
            .onTapGesture {
                navigationVM.pushView(view: AnyView(MerchantView(MerchantVM(merchant))), viewId: CustomerViewIds.MerchantView.rawValue)
            }
        //}
    }
@matteopuc
Copy link
Owner

Hi @optimalpursuits. The issue is that an @EnvironmentObject is not automatically forwarded to your view models, it's something injected and related only to views. Starting from the latest version of this navigation stack (already on master), though, you can create your NavigationStack wherever you want and inject it into the NavigationStackView. This way you'll be able to push/pop programmatically from the outside of a NavigationStackView (for example from within a view model, which is what you want here). Take a look at this very simple example:

import SwiftUI
import NavigationStack

struct RootView: View {
    private let navigationStack = NavigationStack()

    var body: some View {
        NavigationStackView(navigationStack: navigationStack) {
            ContentView(contentVM: ContentVM(navStack: navigationStack))
        }
    }
}

class ContentVM {
    private let navStack: NavigationStack

    init(navStack: NavigationStack) {
        self.navStack = navStack
    }

    func push() {
        navStack.push(Text("Hello World"))
    }
}

struct ContentView: View {
    let contentVM: ContentVM

    var body: some View {
        VStack {
            Text("CONTENT VIEW")
            Button("PUSH") {
                contentVM.push()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        RootView()
    }
}

@Senbazuru1075
Copy link

I'm having the same issue.

I have a rootview controller that is roughly like below.

RegistrationView: View {

var navigationStack: NavigationStack

var body some View {

NavigationStackView {
Button(action: self.navigationstack.push....)
       }
    }
}

the button gives me a fatal error when loading.

@Senbazuru1075
Copy link

It also gives me a fatal error on press

@matteopuc
Copy link
Owner

Hi @Senbazuru1075 it depends on how you create your navigationStack. From the snippet here above is not clear. If you have something like the following is wrong:

struct RootView: View {
    @EnvironmentObject var navigationStack: NavigationStack

    var body: some View {
        NavigationStackView {
            Button("PUSH") {
                navigationStack.push(ChildView())
            }
        }
    }
}

struct ChildView: View {
    var body: some View {
        Text("CHILD")
    }
}

it is wrong because nobody injects a navigation stack as an environment object into the RootView (i.e. the RootView doesn't have a navigation stack).
You have a couple of ways to fix this issue:

  • the "standard" way, where you have your NavigationStackView as the very first view in your SceneDelegate. This way it's the NavigationStackView itself that injects the navigation stack in the hierarchy, so that you can access it with the @EnvironmentObject property wrapper:
//scene delegate file
import SwiftUI
import NavigationStack

@main
struct NavStackExamplesApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationStackView {
                RootView()
            }
        }
    }
}

//your root view file
import SwiftUI
import NavigationStack

struct RootView: View {
    @EnvironmentObject var navigationStack: NavigationStack

    var body: some View {
        Button("PUSH") {
            navigationStack.push(ChildView())
        }
    }
}

struct ChildView: View {
    var body: some View {
        Text("CHILD")
    }
}
  • If you need, instead, to access the navigation stack from the outside of the view hierarchy (for example to implement the coordinator pattern) you have to create the NavigationStack somewhere and then pass it to the NavigationStackView yourself:
//scene delegate file
import SwiftUI
import NavigationStack

@main
struct NavStackExamplesApp: App {
    //this is just a silly example
    //store the navigation stack wherever is ok for you and for your architecture
    static let navigationStack = NavigationStack()

    var body: some Scene {
        WindowGroup {
            NavigationStackView(navigationStack: Self.navigationStack) {
                RootView()
            }
        }
    }
}

//your root view file
import SwiftUI
import NavigationStack

struct RootView: View {
    var body: some View {
        Button("PUSH") {
            NavStackExamplesApp.navigationStack.push(ChildView())
        }
    }
}

struct ChildView: View {
    var body: some View {
        Text("CHILD")
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants