0

I have a NavigationStack with three forms inside it and each form has a couple of screens. How can I share a form's ViewModel only across its screens without having to create an instance globally( inside ContentView). Currently each ViewModel is created whether it's used or not. Here is my code

struct ContentView: View {

    @StateObject private var pathStore = PathStore()

    @StateObject var formOneVM = FormOneViewModel()
    @StateObject var formTwoVM = FormTwoViewModel()

    var body: some View {

        NavigationStack(path: $pathStore.path) {

            HomeView()
            .navigationDestination(FormOneRoutes.self){ route in
                switch route{
                    case .screenOne:
                        FormOneScreenOne()
                    case .screenTwo:
                        FormOneScreenTwo()
            }
            .navigationDestination(FormTwoRoutes.self){ route in
                switch route{
                    case .screenOne:
                        FormTwoScreenOne()
                    case .screenTwo:
                        FormTwoScreenTwo()
        }
    }
    .environmentObject(formOneVM)
    .environmentObject(formTwoVM)
}

I have tried placing each form inside its own NavigationStack but nested NavigationStack doesn't seem to work for me. If you got nested NavigationStack working or have other alternatives please share.

10
  • Do you really need NavigationStack with navigationDestination ? May be you should try to rethink your app. Wanting to absolutely use specific coding pattern is useless if does not fit the need. Commented Jun 28, 2023 at 19:59
  • Hmm what do you suggest? Creating VM instance on first screen then passing along as ObservedObject with NavigationLink ? Commented Jun 28, 2023 at 20:09
  • It depends on what you do in HomeView and how you show Form views for VM A . Commented Jun 28, 2023 at 20:21
  • HomeView is just buttons to navigate to forms. Once each form is complete I return to HomeView . I was using navigationDestination because it was easier to append a route to path based on form state. Commented Jun 28, 2023 at 20:32
  • If I understand, you start creating a VM by tapping a button in HomeView which send you to FormOneVirwOne then push FormOneViewTwo,… when forms is finished go to HomeView and forget Form ? In this case the VM can be declared in as StateObject view one and passed as ObservableObject in subviews. So when back to home views it’s gone. Commented Jun 28, 2023 at 20:44

1 Answer 1

0

An example with a view model created only when doing into a specific form hierarchy. First level use navigationDestination, then navigationLink and binding :

class PathStore: ObservableObject {
    @Published var path: NavigationPath = NavigationPath()
    func gotoToTop() {
        path.removeLast()
    }
}

class FormOneViewModel: ObservableObject {
    @Published var oneDone = false
    @Published var twoDone = false
}

class FormTwoViewModel: ObservableObject {
    // view one entries
    @Published var entry11 = false
    @Published var entry12 = false
    
    // view two entries
    @Published var entry21 = false
    @Published var entry22 = false
}

enum FormOneRoutes: Hashable {
    case screenOne
    case screenTwo

}

enum FormTwoRoutes: Hashable {
    case screenOne
    case screenTwo
}

struct ContentView: View {
    
    @StateObject private var pathStore = PathStore()
    
    var body: some View {
        
        NavigationStack(path: $pathStore.path) {
            HomeView()
            // here just to start first view of first model
                .navigationDestination(for: FormOneRoutes.self){ route in
                    FormOneScreenOne()
                }
            // here just to start first view of second model
                .navigationDestination(for: FormTwoRoutes.self){ route in
                    FormTwoScreenOne()
                }
        }
        .environmentObject(pathStore)
    }
}


struct HomeView: View {
    var body: some View {
        VStack {
            NavigationLink(value: FormOneRoutes.screenOne) {
                Text("Form one")
            }

            NavigationLink(value: FormTwoRoutes.screenOne) {
                Text("Form Two")
            }

        }
    }
}

struct FormOneScreenOne: View {
    // model only exists while in first form hierachy
    @StateObject var formVM = FormOneViewModel()
    var body: some View {
        VStack {
            Text("FormOneScreenOne \(formVM.oneDone ? "1" : "-") \(formVM.twoDone ? "2" : "-")")
            Toggle("One", isOn: $formVM.oneDone)
            NavigationLink {
                FormOneScreenTwo(formVM: formVM)
            } label: {
                Text("-> Form one screen two")
            }
        }
    }
}

struct FormOneScreenTwo: View {
    @ObservedObject var formVM: FormOneViewModel
    @EnvironmentObject var pathStore: PathStore
    var body: some View {
        VStack {
            Text("FormOneScreenTwo \(formVM.oneDone ? "1" : "-") \(formVM.twoDone ? "2" : "-")")
            Toggle("Two", isOn: $formVM.twoDone)
            Button {
                pathStore.gotoToTop()
            } label: {
                Text("Done")
            }
        }
    }
}

enum YesNo {
    case yes
    case no
    case undefined
}

struct FormTwoScreenOne: View {
    // model only exists while in second form hierachy
    @StateObject var formVM = FormTwoViewModel()
    @State var yesNo1: YesNo = .undefined
    @State var yesNo2: YesNo = .undefined
    var allDefined: Bool {
        yesNo1 != .undefined && yesNo2 != .undefined
    }
    @State var allEntriesDone: Bool = false
    var body: some View {
        VStack {
            Text("FormTwoScreenOne \(yesNo1 == .undefined ? "--" : (formVM.entry11 ? "+1" : "-1")) \(yesNo2 == .undefined ? "--" : ( formVM.entry12 ? "+2" : "-2"))")
            Picker("Choose entry 1", selection: $yesNo1) {
                Text("Yes").tag(YesNo.yes)
                Text("No").tag(YesNo.no)
            }
            Picker("Choose entry 2", selection: $yesNo2) {
                Text("Yes").tag(YesNo.yes)
                Text("No").tag(YesNo.no)
            }
        }
        .onChange(of: yesNo1) { newValue in
            set(value: newValue, for: $formVM.entry11)
        }
        .onChange(of: yesNo2) { newValue in
            set(value: newValue, for: $formVM.entry12)
        }
        .navigationDestination(isPresented: $allEntriesDone) {
            FormTwoScreenTwo(formVM: formVM)
        }
    }
    
    func set(value: YesNo, for bind: Binding<Bool>) {
        switch value {
            case .yes:
                bind.wrappedValue = true
            case .no:
                bind.wrappedValue = false
            case .undefined:
                break
        }
        allEntriesDone = allDefined
    }
}

struct FormTwoScreenTwo: View {
    @EnvironmentObject var pathStore: PathStore
    @ObservedObject var formVM: FormTwoViewModel
    @State var yesNo1: YesNo = .undefined
    @State var yesNo2: YesNo = .undefined
    var allDefined: Bool {
        yesNo1 != .undefined && yesNo2 != .undefined
    }
    var body: some View {
        VStack {
            Text("FormTwoScreenTwo \(yesNo1 == .undefined ? "--" : (formVM.entry21 ? "+1" : "-1")) \(yesNo2 == .undefined ? "--" : ( formVM.entry22 ? "+2" : "-2"))")
            Picker("Choose entry 1", selection: $yesNo1) {
                Text("Yes").tag(YesNo.yes)
                Text("No").tag(YesNo.no)
            }
            Picker("Choose entry 2", selection: $yesNo2) {
                Text("Yes").tag(YesNo.yes)
                Text("No").tag(YesNo.no)
            }
            if allDefined {
                Text("Everything entered")
            }
            Button {
                pathStore.gotoToTop()
            } label: {
                Text("Exit")
            }
        }
        .onChange(of: yesNo1) { newValue in
            set(value: newValue, for: $formVM.entry21)
        }
        .onChange(of: yesNo2) { newValue in
            set(value: newValue, for: $formVM.entry22)
        }
    }
    
    func set(value: YesNo, for bind: Binding<Bool>) {
        switch value {
            case .yes:
                bind.wrappedValue = true
            case .no:
                bind.wrappedValue = false
            case .undefined:
                break
        }
    }
}
Sign up to request clarification or add additional context in comments.

5 Comments

Correct me if I'm wrong but doesn't this mean the NavigationLink is always active whether oneDone is true or false ? So user can navigate to screen two without filling screen one
I just made a simple test to explain how to have only one active VM. I do not know what you want to do in your app and the logic you want. You can set the navigation link inside a test which will be only true when a screen is complete.
Yeah I understand, thanks. Could you explain what you mean by ‘set the navigationlink inside a test’ If it was the old navigationlink api I could just set its isActive property to match my form but that’s deprecated now.
@peeta: Made a little change in second form to handle conditional navigation and conditional display of text.
Hey Ptit, when I tried this approach I am getting error saying I can't use navigationDestination inside child views ( more info here )

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.