TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

How do you pass the navigation path through several views?

Forums > SwiftUI

I've implemented a navigation stack that appends the next view's view model so that the appending and .navigationDestination looks like the below. Whenever I press the continue button to add the viewModel to the path in the ContentView (the child of BumperScreen and the second in the path), the app just stops. I tried isolating the problem with a test project using the same methodology for navigation and have been able to navigate several views, almost infinitely. I cannot replicate this issue and am having a hard time understanding why one is working, and not the other.

struct BumperScreen: View {
    @State private var path = NavigationPath()
    @StateObject var viewModel: BumperScreenViewModel
    @StateObject var sheetManager = SheetManager()

    init(viewModel: @autoclosure @escaping () -> BumperScreenViewModel) {
        self._viewModel = .init(wrappedValue: viewModel())
    }

    var body: some View {
        if isDoneOnboarding {
            HomeView()
                .environmentObject(sheetManager)
        } else {
            NavigationStack(path: $path) {
                ZStack {
                    Color(.Gold)
                        .ignoresSafeArea()
                    VStack {
                        if show {
                            Spacer()
                            loadAnimation
                                .frame(width: 150, height: 150)
                                .task {
                                    try? await viewModel.getDataFromAPI()
                                    try? await Task.sleep(for: Duration.seconds(1))
                                    path.append(ContentViewViewModel())
                                    doneLoading.toggle()
                                    show.toggle()
                                }
                            Spacer()
                        } else if doneLoading {
                            EmptyView()
                        } else {
                            launchAnimation
                        }
                    }
                    .navigationDestination(for: ContentViewViewModel.self) { model in
                        ContentView(viewModel: model, path: $path)
                            .environmentObject(sheetManager)
                    }
                }
            }
        }
    }
}

Which then leads the user to the second view

struct ContentView: View {
    @EnvironmentObject var sheetManager: SheetManager
    @StateObject var viewModel: ContentViewViewModel
    @Binding var path: NavigationPath

    var body: some View {
            ZStack {
                Color(.trulliGold)
                    .ignoresSafeArea()
                VStack {
                    Spacer()
                    headerStatement
                    Spacer()
                    VStack {
                        privacyPolicy
                        bluetoothPolicy
                        locationsPolicy
                        notificationsPolicy
                        apprunningPolicy
                    }
                    Spacer()
                    Spacer()
                    continueButton
                }
            }
            .popup(with: sheetManager)
            .navigationBarBackButtonHidden()
    }

    var continueButton: some View {
        Button(action: {
            print("pressed")
            path.append(WelcomeScreenViewViewModel())
        }) {
            Text("CONTINUE")
        }
        .navigationDestination(for: WelcomeScreenViewViewModel.self) { model in
            WelcomeScreenView(viewModel: model, path: $path)
        }
    }
}

Which goes to

struct WelcomeScreenView: View {
    @StateObject var viewModel: WelcomeScreenViewViewModel
    @Binding var path: NavigationPath

    var body: some View {
            ZStack {
                Color(.trulliGold)
                    .ignoresSafeArea()
                VStack(alignment: .center, spacing: 8) {
                    header
                    Spacer()
                    tabView
                    Spacer()
                    Spacer()
                    nextButton
                    closeButton
                }
            }
            .navigationDestination(for: SetupGuideSpeakerSearchViewViewModel.self) { model in
                SetupGuideSpeakerSearchView(viewModel: model, path: $path )
            }
        .navigationBarBackButtonHidden()
    }
}

2      

Have you tried to replace your button with NavLink instead? Something like this?

    var continueButton: some View {
       NavigationLink(value: yourModel) {
                Text("Continue")
        }
        .navigationDestination(for: WelcomeScreenViewViewModel.self) { model in
            WelcomeScreenView(viewModel: model, path: $path)
        }
    }

2      

@ygeras Yes, I have and still no luck. If i remove the path from the SetupGuideSpeakerSearchView so that its initializer doesn't have to take in a path it works fine, meaning I can reach WelcomeScreenView. But i cannot navigate beyond that which is why i need the path to be there. I've created a test project and have implemented the same methodology for navigation to try and isolate the issue and debug, and have been unable to reproduce meaning i can navigate many views with passing path as a binding to every subsequent view.

debug project

every view is set up in this way and i can go 5 layers deep

struct NextViewOptionTwoView: View {
    @StateObject var viewModel: NextViewOptionTwoViewModel
    @Binding var path: NavigationPath
    var body: some View {
        VStack {
            Text("\(viewModel.name), you are number \(viewModel.id)")
            Text("Maybe i made it")
            Button(action: {
                path.append(NextViewOptionThreeViewModel(name: viewModel.name))
            }) {
                Text("Do it Again")
            }
        }
        .navigationDestination(for: NextViewOptionThreeViewModel.self ) { model in
            NextViewOptionThreeView(viewModel: model, path: $path)
        }
        .navigationBarBackButtonHidden()
    }
}

2      

Hacking with Swift is sponsored by String Catalog.

SPONSORED Get accurate app localizations in minutes using AI. Choose your languages & receive translations for 40+ markets!

Localize My App

Sponsor Hacking with Swift and reach the world's largest Swift community!

Did you invent the approach of having a custom init in your BumperScreen view, or did see that in a tutorial or blog post? If the latter, I'd be very interested in a reference to it.

I suspect you should replace the init with an .onAppear view modifier. A view is a struct that is re-instantiated every time any element of the view changes, so your init would be called every time this happens. In contrast, .onAppear is called only when the view is first displayed.

This article might be helpful: https://swiftbysundell.com/articles/the-lifecycle-and-semantics-of-a-swiftui-view/

2      

@bobstern it wasn't in a tutorial, it was from a project I had worked on in the past. I remember being shared a couple articles on it but I can't seem to track them down to share the link. Mostly because it appears I can't find the right way to enter my question into google. But it had something to do memory management I believe and lifecycles.

However, I did try using your suggestion, and I'm still unable to proceed. This is very strange

2      

@kenada_95,

Try to check first if path is actually added where you have the problem. NavPath has property count, so you can check how many there are. So at least you can know if this is issue with NavPath, or this is the view which is not instantiated...

UPD:

Also just thinking further... Maybe to try instantiate the model in current view and then pass it further as ready object. I suppose you press the button "CONTINUE" and your model instatiated at the same moment, right? And and the same time navigation happens, mmmm maybe there is not enought time to instantiate model to navigate forward....

In you debug project models are classes that contain just a string and an int, so it doesn't take time to instantiate them. In your real project who knows...

2      

@ygeras,

I've found the count to be about what i expected it to be, its not nil by any means but this is the print out from it "NavigationPath(items: SwiftUI.NavigationPath.(unknown context at $10a317200).Representation.eager([SwiftUI.(unknown context at $10a316e78).ItemBox<SplashScreenExample.ContentViewViewModel>]), subsequentItems: [], iterationIndex: 0)" when i press "Continue" in ContentView. However I tried your method of instantiating the WelcomeScreens view model in content view before appending it I have found that i can't even reach the ContentView itself. It stays stuck on the loading animation in the bumper screen. To make this happen I have posted the code below and left in some of the bools I have in the view.

struct ContentView: View {
    @EnvironmentObject var sheetManager: SheetManager
    @StateObject var viewModel: ContentViewViewModel
    @State var privacyPolicyChecked: Bool = false
    @State var bluetoothPolicyChecked: Bool = false
    @State var locationsPolicyChecked: Bool = false
    @State var notificationsPolicyChecked: Bool = false
    @State var apprunningPolicyChecked: Bool = false
    @State var isGoingToNewView = false
    @Binding var path: NavigationPath
    private var areAllChecked: Bool {
        return privacyPolicyChecked && bluetoothPolicyChecked && locationsPolicyChecked && notificationsPolicyChecked && apprunningPolicyChecked
    }

    @State var nextModel: WelcomeScreenViewViewModel = WelcomeScreenViewViewModel()

    var body: some View {
            ZStack {
                Color(.trulliGold)
                    .ignoresSafeArea()
                VStack {
                    Spacer()
                    headerStatement
                    Spacer()
                    VStack {
                        privacyPolicy
                        bluetoothPolicy
                        locationsPolicy
                        notificationsPolicy
                        apprunningPolicy
                    }
                    Spacer()
                    Spacer()
                    continueButton
                }
            }
            .popup(with: sheetManager)
            .navigationBarBackButtonHidden()
    }

    var continueButton: some View {
        Button(action: {
            print("pressed")
            print(path)
            path.append(nextModel)

        }) {
            Text("CONTINUE")
                .foregroundColor(.white)
        }
        .disabled(!self.areAllChecked)
        .padding()
        .background(Color.orange)
        .foregroundColor(.white)
        .clipShape(Capsule())
        .padding(.bottom, 32)
        .navigationDestination(for: WelcomeScreenViewViewModel.self) { model in
            WelcomeScreenView(viewModel: model, path: $path)
        }
    }
 }

2      

And what is the function of this modifier in ContentView? Without detailed view of the code no more ideas :(

.popup(with: sheetManager)

2      

@ygeras is a custom property to toggle popups. I've toyed with this before as well, removed the sheet manager as a environment property, etc. and its still the same bug. I wonder if this is something that needs to be filed with Apple.

  func popup(with sheetManager: SheetManager) -> some View {
        self.modifier(PopupViewModifier(sheetManager: sheetManager){})
    }
struct PopupViewModifier: ViewModifier {
    @ObservedObject var sheetManager: SheetManager
    var popupAction: () -> (Void)

    func body(content: Content) -> some View {
        content
            .disabled(sheetManager.action.isPresented ? true : false )
            .blur(radius: sheetManager.action.isPresented ? 05 : 0)
            .overlay(alignment: .center) {
                if case let .present(config) = sheetManager.action {
                    switch config.type {
                    case .popupAlert:
                        PopupView(config: config) {
                            withAnimation {
                                sheetManager.dismiss()
                            }
                        }
                    case .soundProfileList:
                        SoundProfilePopupView(config: config) {
                            withAnimation {
                                sheetManager.dismiss()
                            }
                        } tappedProfile: {
                            popupAction()
                        }
                        .padding(.horizontal)
                    }
                }
            }
            .ignoresSafeArea()
    }
}

2      

Well if you sample project works without issues, means that Apple frameworks work as intended. Most probably there is something in your code that prevents navigating. You'd better deconstruct completely your code and by adding logic check where the issue starts. Or using debugger go into details what is going on behind the scenes. But that cryptic data from debugger is very often indecipherable for most of us ))))

Have you tried to set up navigation without handling navPaths and passing data. But just simply using NavigationLinks and let SwiftUI take care of the rest? As I noticed, you do not really passing data around but instantiating datamodels and then pass that objects to your views. What if instead let the view instantiate model itself. Something simple as in the below code: (well, if it fits your logic of course)

class Model1 {
    var name = "Model 1"
}

class Model2 {
    var name = "Model 2"
}

class Model3 {
    var name = "Model 3"
}

struct ContentView: View {

    var body: some View {
        NavigationStack {
            VStack(spacing: 20) {
                Text("This is main screen")

                NavigationLink("Navigate to Model1") {
                    ModelView1()
                }
                .buttonStyle(.borderedProminent)
            }
        }
    }
}

struct ModelView1: View {
    @State var model = Model1()

    var body: some View {
        VStack(spacing: 20) {
            Text(model.name)

            NavigationLink("Navigate to Model2") {
                ModelView2()
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

struct ModelView2: View {
    @State var model = Model2()

    var body: some View {
        VStack(spacing: 20) {
            Text(model.name)

            NavigationLink("Navigate to Model3") {
                ModelView3()
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

struct ModelView3: View {
    @State var model = Model3()

    var body: some View {
        VStack(spacing: 20) {
            Text(model.name)
        }
    }
}

2      

@ygeras thank you for your help, i believe I found the problem to be not specific to how i'm navigating but is specific to the WelcomeScreenView. I've navigated from ContentView to the view "after" welcomescreen and functions as normal. So the methodology i'm using isn't wrong, it's something specific to this WelcomeScreenView and for whatever reason the binding to the Path within this view since removing the path still allows me to reach it. So I decided to post the whole screen and not leave out anything.

struct WelcomeScreenView: View {
    private let configCollection: [SheetManager.Config] = [SheetManager.Config.init(systemName: "Unknown",
                                                                                    title: "BASS50 Support",
                                                                                    content: "Guided setup and advanced controls allow you to bring bass anywhere", type: .popupAlert),
                                                           SheetManager.Config.init(systemName: "Unknown",
                                                                                    title: "Quick Sound Profile Switching",
                                                                                    content: "At home or on the go, match your speaker's sound to your environment in seconds", type: .popupAlert),
                                                           SheetManager.Config.init(systemName: "Unknown",
                                                                                    title: "Music Control",
                                                                                    content: "Easily control the playback, volume, and bass gain of your music", type: .popupAlert)]
    var isNotLastPhoto: Bool {
        return currentIndex < 2
    }
    let numberOfTabs = 3
    @State var currentIndex = 0
    @State var isGoingToNewView: Bool = false
    @StateObject var viewModel: WelcomeScreenViewViewModel
    @Binding var path: NavigationPath

    var body: some View {
            ZStack {
                Color(.trulliGold)
                    .ignoresSafeArea()
                VStack(alignment: .center, spacing: 8) {
                    header
                    Spacer()
                    tabView
                    Spacer()
                    Spacer()
                    nextButton
                    closeButton
                }
            }
            .navigationDestination(for: SetupGuideSpeakerSearchViewViewModel.self) { model in
                SetupGuideSpeakerSearchView(viewModel: model, path: $path )
            }
        .navigationBarBackButtonHidden()
    }

    var header: some View {
        Text("What's New")
            .font(Font.semibold24PN())
    }

    var tabView: some View {
        TabView(selection: $currentIndex) {
            ForEach(0..<numberOfTabs) { num in
                WelcomeScreenTabbedView(config: configCollection[num])
            }
        }.tabViewStyle(PageTabViewStyle())
    }

    var nextButton: some View {
        Button(action: {
            if isNotLastPhoto {
                withAnimation {
                    currentIndex = currentIndex + 1
                }
            } else {
//                path.append(GreaterViewOptions.speakerSetUpStart)
            }
        }) {
            Text(isNotLastPhoto ? "NEXT" : "Continue")
                .foregroundColor(.white)
        }
        .padding()
        .frame(width: 200)
        .background(Color.orange)
        .foregroundColor(.white)
        .clipShape(Capsule())
        .padding(.top, 32)
    }

    var closeButton: some View {
        Button(action: {
            path.append(SetupGuideSpeakerSearchViewViewModel())
        }) {
            Text("Close")
                .foregroundColor(Color.orange)
                .underline()
        }
        .padding()
        .frame(width: 200)
        .foregroundColor(Color.orange)
        .clipShape(Capsule())
    }

}

2      

@Bnerd  

I have an Observable Model, where I use a

@Published var path = NavigationPath()

My model is @EnvironmentObject for all child views, the rest is easy :)

2      

Hacking with Swift is sponsored by String Catalog.

SPONSORED Get accurate app localizations in minutes using AI. Choose your languages & receive translations for 40+ markets!

Localize My App

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.