I am struggling to get my head around how to use programmatic navigation with multiple destination views which take the same type of value. In the following code I can successfully navigate from ContentView to View2, but would like to navigate from View2 to View3 by adding a value to the path.
The navigationDestination in ContentView has View2 specified. How/where do I add a second navigationDestination to View3? If I add a navigationDestination in View2 pointing to View3 then it doesn't work, as it uses the View1's navigationDestination as it is closer to root. I would appreciate some guidance on how to approach this problem. Many thanks in advance.
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
NavigationLink(value: "view2") {
Text("Go to View2")
}
.navigationDestination(for: String.self) { destination in
View2(someParameterA: destination)
}
.navigationTitle("ContentView")
}
}
}
struct View2: View {
#State var someParameterA: String
var body: some View {
VStack {
Text(someParameterA)
NavigationLink(value: "view3") {
Text("Go to View3")
}
}
.navigationTitle("View 2")
}
}
struct View3: View {
#State var someParameterB: String
var body: some View {
Text(someParameterB)
.navigationTitle("View 3")
}
}
I've managed to hack the following solution together which works but is there a better approach?
enum DestinationView {
case view2
case view3
}
struct NavStruct: Equatable, Hashable {
var destinationView: DestinationView
var param: String
}
class ViewSelector {
#ViewBuilder
static func viewForDestination(_ destination: DestinationView, _ param: String) -> some View {
switch destination {
case .view2:
View2(someParameterA: param)
case .view3:
View3(someParameterB: param)
}
}
}
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
NavigationLink(value: NavStruct(destinationView: .view2, param: "view2")) {
Text("Go to View2")
}
.navigationDestination(for: NavStruct.self) { destination in
ViewSelector.viewForDestination(destination.destinationView, destination.param)
}
.navigationTitle("ContentView")
}
}
}
struct View2: View {
#State var someParameterA: String
var body: some View {
VStack {
Text(someParameterA)
NavigationLink(value: NavStruct(destinationView: .view3, param: "view3")) {
Text("Go to View3")
}
}
.navigationTitle("View 2")
}
}
struct View3: View {
#State var someParameterB: String
var body: some View {
VStack {
Text(someParameterB)
}
.navigationTitle("View 3")
}
}
Related
I have a View with a search button in the toolbar. The search button presents a sheet to the user and when he clicks on a result I would like the sheet to be dismissed and a detailView to be opened rather than navigating to the detailView from inside the sheet. The dismiss part is easy, but how do I open the detailView in the NavigationStack relative to the original View that presented the Sheet?
I'm also getting an error on the navigationStack initialization.
HomeScreen:
struct CatCategoriesView: View {
#StateObject private var vm = CatCategoriesViewModel(service: Webservice())
#State var showingSearchView = false
#State var path: [CatDetailView] = []
var body: some View {
NavigationStack(path: $path) { <<-- Error here "No exact matches in call to initializer "
ZStack {
Theme.backgroundColor
.ignoresSafeArea()
ScrollView {
switch vm.state {
case .success(let cats):
LazyVStack {
ForEach(cats, id: \.id) { cat in
NavigationLink {
CatDetailView(cat: cat)
} label: {
CatCategoryCardView(cat: cat)
.padding()
}
}
}
case .loading:
ProgressView()
default:
EmptyView()
}
}
}
.navigationTitle("CatPedia")
.toolbar {
Button {
showingSearchView = true
} label: {
Label("Search", systemImage: "magnifyingglass")
}
}
}
.task {
await vm.getCatCategories()
}
.alert("Error", isPresented: $vm.hasError, presenting: vm.state) { detail in
Button("Retry") {
Task {
await vm.getCatCategories()
}
}
} message: { detail in
if case let .failed(error) = detail {
Text(error.localizedDescription)
}
}
.sheet(isPresented: $showingSearchView) {
SearchView(vm: vm, path: $path)
}
}
}
SearchView:
struct SearchView: View {
let vm: CatCategoriesViewModel
#Environment(\.dismiss) private var dismiss
#Binding var path: [CatDetailView]
#State private var searchText = ""
var body: some View {
NavigationStack {
List {
ForEach(vm.filteredCats, id: \.id) { cat in
Button(cat.name) {
dismiss()
path.append(CatDetailView(cat: cat))
}
}
}
.navigationTitle("Search")
.searchable(text: $searchText, prompt: "Find a cat..")
.onChange(of: searchText, perform: vm.search)
}
}
}
It can be a little tricky, but I'd suggest using a combination of Apple's documentation on "Control a presentation link programmatically" and shared state. To achieve the shared state, I passed a shared view model into the sheet.
I have simplified your example to get it working in a more generic way. Hope this will work for you!
ExampleParentView.swift
import SwiftUI
struct ExampleParentView: View {
#StateObject var viewModel = ExampleViewModel()
var body: some View {
NavigationStack(path: $viewModel.targetDestination) {
List {
NavigationLink("Destination A", value: TargetDestination.DestinationA)
NavigationLink("Destination B", value: TargetDestination.DestinationB)
}
.navigationDestination(for: TargetDestination.self) { target in
switch target {
case .DestinationA:
DestinationA()
case .DestinationB:
DestinationB()
}
}
.navigationTitle("Destinations")
Button(action: {
viewModel.showModal = true
}) {
Text("Click to open sheet")
}
}
.sheet(isPresented: $viewModel.showModal, content: {
ExampleSheetView(viewModel: viewModel)
.interactiveDismissDisabled()
})
}
}
ExampleViewModel.swift
import Foundation
import SwiftUI
class ExampleViewModel: ObservableObject {
#Published var showModal = false
#Published var targetDestination: [TargetDestination] = []
}
enum TargetDestination {
case DestinationA
case DestinationB
}
ExampleSheetView.swift
import SwiftUI
struct ExampleSheetView: View {
let viewModel: ExampleViewModel
var body: some View {
VStack {
Text("I am the sheet")
Button(action: {
viewModel.showModal = false
viewModel.targetDestination.append(.DestinationA)
}) {
Text("Close the sheet and navigate to `A`")
}
Button(action: {
viewModel.showModal = false
viewModel.targetDestination.append(.DestinationB)
}) {
Text("Close the sheet and navigate to `B`")
}
}
}
}
DestinationA.swift
import SwiftUI
struct DestinationA: View {
var body: some View {
Text("Destination A")
}
}
DestinationB.swift
import SwiftUI
struct DestinationB: View {
var body: some View {
Text("Destination B")
}
}
I am new to SwiftUI framework I am trying to implement NavigationStack. I want to navigate on button action instead of using NavigationLink. The reason behind that is, I need to navigate once a particular function get performed on button action.
struct AView: View {
#State private var actionss = [Int]()
var body: some View {
NavigationStack(path:$actionss) {
VStack{
Button("test") {
actionss.append(0)
}
}
.navigationDestination(for: Int.self) { data in
BView()
}
}
}
}
Above code of "AView" is working fine to navigate "BView". The only thing is I am not able to navigate on "CView" from "BView" without using NavigationLink.
I need to perform particular function before navigate from "BView" to "CView" as well.
Please help me in this.
Thank you in advance.
Assuming that the work is done on BView you can use .navigationDestination as well:
struct AView: View {
#State private var actionss = [Int]()
var body: some View {
NavigationStack(path:$actionss) {
VStack{
Button("show BView") {
actionss.append(0)
}
}
.navigationDestination(for: Int.self) { data in
BView()
}
.navigationTitle("AView")
}
}
}
struct BView: View {
#State var show: Bool = false
var body: some View {
VStack {
Button("show CView") {
show = true
}
}
.navigationDestination(isPresented: $show) {
CView()
}
.navigationTitle("BView")
}
}
struct CView: View {
var body: some View {
Text("Hello")
.navigationTitle("CView")
}
}
I'm trying to find the reason why .listRowBackground is not updated when a new item has been added to the list. Here is my code sample:
#main
struct BGtestApp: App {
#ObservedObject var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
}
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
NavigationView {
List {
ForEach(vm.items, id: \.self) { item in
NavigationLink {
DetailView().environmentObject(vm)
} label: {
Text(item)
}
.listRowBackground(Color.yellow)
}
}
}
}
}
struct DetailView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
Button("Add new") {
vm.items.append("Ananas")
}
}
}
How it looks like:
TIA for you help!
You can force the list to refresh when you cone back to the list. You can tag an id for your list by using .id(). Here is my solution:
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
#State private var viewID = UUID()
var body: some View {
NavigationView {
List {
ForEach(vm.items, id: \.self) { item in
NavigationLink {
DetailView()
.environmentObject(vm)
} label: {
Text(item)
}
}
.listRowBackground(Color.yellow)
}
.id(viewID)
.onAppear {
viewID = UUID()
}
}
}
}
Hope it's helpful for you.
The solution I found is not ideal, but should work. What I did is made items to be #State variable :
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
#State var items: [String] = []
var body: some View {
NavigationView {
VStack {
List {
ForEach(items, id: \.self) { item in
NavigationLink {
DetailView().environmentObject(vm)
} label: {
RowView(item: item)
}
.listRowBackground(Color.yellow)
}
}
.onAppear {
self.items = vm.items
}
}
}
Coming from Android and working on a very complex application , i would like to use NavigationView as much as possible. Having one view and make all elements appear and disappear on this view seems impossible to handle for me .
I was using navigationView to navigate bewteen views with navigationBar hidden .
This way navigating or making view appear is transparent for the user
After some tests , i encounter limitations : at the 13th or 14 th level of navigation everything disappear and app basically crashes .
Once more , this is a direct navigation between 2 content views , no HOMESCREEN
import SwiftUI
struct test4: View {
#State private var intent3: Bool = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination : test3() , isActive : $intent3) { }
Text("ver 4")
.onTapGesture {
intent3 = true }
Spacer()
}
}
.navigationBarHidden(true)
}
}
import SwiftUI
struct test3: View {
#State private var intent4: Bool = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination : test4() , isActive : $intent4) { }
Text("ver 3")
.onTapGesture {
intent4 = true }
Spacer()
}
}.navigationBarHidden(true) }
}
Here a basic example of navigation directly between 2 contents views . Crashes after 14/15 clicks. I encounter the same issue with about any navigation link.
Update:
With your added code, I can see the initial crash was a result of adding a new NavigationView each time. This solves it:
struct ContentView: View {
var body: some View {
NavigationView {
Test3()
}
}
}
struct Test4: View {
#State private var intent3: Bool = false
var body: some View {
VStack{
NavigationLink(destination : Test3() , isActive : $intent3) { }
Text("ver 4")
.onTapGesture {
intent3 = true
}
Spacer()
}
.navigationBarHidden(true)
}
}
struct Test3: View {
#State private var intent4: Bool = false
var body: some View {
VStack{
NavigationLink(destination : Test4() , isActive : $intent4) { }
Text("ver 3")
.onTapGesture {
intent4 = true }
Spacer()
}
.navigationBarHidden(true)
}
}
Original answer:
However, there are solutions to pop to the top of a navigation hierarchy.
One way is to use isActive to manage whether or not a given NavigationLink is presenting its view. That might look like this:
class NavigationReset : ObservableObject {
#Published var rootIsActive = false
func popToTop() {
rootIsActive = false
}
}
struct ContentView: View {
#StateObject private var navReset = NavigationReset()
var body: some View {
NavigationView {
NavigationLink(destination: DetailView(title: "First"), isActive: $navReset.rootIsActive) {
Text("Root nav")
}
}.environmentObject(navReset)
}
}
struct DetailView : View {
var title : String
#EnvironmentObject private var navReset : NavigationReset
var body: some View {
VStack {
NavigationLink(destination: DetailView(title: "\(Date())")) {
Text("Navigate (\(title))")
}
Button("Reset nav") {
navReset.popToTop()
}
}
}
}
Another trick you could use is changing an id on a NavigationLink -- as soon as that happens, it re-renders and becomes inactive.
class NavigationReset : ObservableObject {
#Published var id = UUID()
func popToTop() {
id = UUID()
}
}
struct ContentView: View {
#StateObject private var navReset = NavigationReset()
var body: some View {
NavigationView {
NavigationLink(destination: DetailView(title: "First")) {
Text("Root nav")
}
.id(navReset.id)
}.environmentObject(navReset)
}
}
struct DetailView : View {
var title : String
#EnvironmentObject private var navReset : NavigationReset
var body: some View {
VStack {
NavigationLink(destination: DetailView(title: "\(Date())")) {
Text("Navigate (\(title))")
}
Button("Reset nav") {
navReset.popToTop()
}
}
}
}
It works by marking the first NavigationLink (ie the one on the Home Screen) with an id. As soon as that id is changed, the NavigationLink is recreated, popping all of the views off of the stack.
I have a navigation stack that's not quite working as desired.
From my main view, I want to switch over to a list view which for the sake of this example represents an array of strings.
I want to then navigate to a detail view, where I want to be able to change the value of the selected string.
I have 2 issues with below code:
on the very first keystroke within the TextField, the detail view is being dismissed
the value itself is not being changed
Also, I suppose there must be a more convenient way to do the binding in the detail view ...
Here's the code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
TestMainView()
}
}
}
struct TestMainView: View {
var body: some View {
NavigationView {
List {
NavigationLink("List View", destination: TestListView())
}
.navigationTitle("Test App")
}
}
}
struct TestListView: View {
#State var strings = [
"Foo",
"Bar",
"Buzz"
]
#State var selectedString: String? = nil
var body: some View {
List(strings.indices) { index in
NavigationLink(
destination: TestDetailView(selectedString: $selectedString),
tag: strings[index],
selection: $selectedString) {
Text(strings[index])
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("List")
}
}
}
struct TestDetailView: View {
#Binding var selectedString: String?
var body: some View {
VStack {
if let _ = selectedString {
TextField("Placeholder",
text: Binding<String>( //what's a better solution here?
get: { selectedString! },
set: { selectedString = $0 }
)
)
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
Spacer()
}
.navigationTitle("Detail")
}
}
struct TestMainView_Previews: PreviewProvider {
static var previews: some View {
TestMainView()
}
}
I am quite obviously doing it wrong, but I cannot figure out what to do differently...
You're changing the NavigationLink's selection from inside the NavigationLink which forces the TestListView to reload.
You can try the following instead:
struct TestListView: View {
#State var strings = [
"Foo",
"Bar",
"Buzz",
]
var body: some View {
List(strings.indices) { index in
NavigationLink(destination: TestDetailView(selectedString: self.$strings[index])) {
Text(self.strings[index])
}
}
}
}
struct TestDetailView: View {
#Binding var selectedString: String // remove optional
var body: some View {
VStack {
TextField("Placeholder", text: $selectedString)
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
Spacer()
}
}
}