navigationDestination(isPresented) with a path in swiftui 4 - swiftui

Im trying to pop back to a specific view point or the root view with navigationDestination(isPresented) being used to push views.
Here is a simpler version of the code I am working with
import SwiftUI
struct View1: View {
#State var goToView2 = false
#State var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
VStack {
Text("View 1")
Button("Go to View 2") {
goToView2 = true
}
}.navigationDestination(isPresented: $goToView2) {
View2(path: $path)
}
}
}
}
struct View2: View {
#State var goToView3 = false
#Binding var path: NavigationPath
var body: some View {
VStack {
Text("View 2")
Button("Go to View 3") {
goToView3 = true
}
}.navigationDestination(isPresented: $goToView3) {
View3(path: $path)
}
}
}
struct View3: View {
#Binding var path: NavigationPath
var body: some View {
VStack {
Text("View 3")
Button("Go to View 1") {
print("Before: \(path.count)")
path = .init()
print("After: \(path.count)")
}
}
}
}
I don't exactly know what else to try. I've tried appending to the path value on change, but as expected, that does not work. Also, the path count is set to 0. Any help is appreciated
Here is the result of the previous code:
https://i.stack.imgur.com/QAWZN.gif

Related

SwiftUI, How to Use Programmatic Navigation Using Value, with different NavigationDestinations

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")
}
}

iOS15 NavigationView lost the state after putting app in background and bring it back

In the app, if I follow the navigations from MainView->Tab1->Link1->Sub Link1, put the app to background, then bring it back, then it shows back to Tab1 again, does anyone know why NavigationView cannot keep the last view? it works fine in iOS 14.7
struct LocalNotificationDemoView: View {
#StateObject var localNotification = LocalNotification()
#ObservedObject var notificationCenter: NotificationCenter
var body: some View {
NavigationView {
VStack {
MainView()
}
}
.navigationViewStyle(.stack)
}
}
struct MainView: View {
var body: some View {
TabView {
Tab1()
.tabItem {
Text("Tab1")
}
Tab2()
.tabItem {
Text("Tab2")
}
}
}
}
struct Tab1: View {
#State var selection: Int? = nil
var body: some View {
NavigationLink(destination: View1(), tag: 1, selection: $selection) {
Text("Link 1")
}
}
}
struct Tab2: View {
#State var selection: Int? = nil
var body: some View {
NavigationLink(destination: Text("Link 2").navigationTitle("").navigationBarHidden(true), tag: 1, selection: $selection) {
Text("Link 2")
.onAppear {
print("Link2 shows")
Thread.callStackSymbols.forEach{print($0)}
}
}
}
}
struct View1: View {
#State var selection: Int? = nil
var body: some View {
NavigationLink(destination: Text("Sub Link 1"), tag: 1, selection: $selection) {
Text("Sub Link 1")
}
}
}

Wrong List Item Selected in NavigationLink

I'm trying to build out a simple navigation where you can click on items in a link and pop back to the root controller from a sheet view. As you can see from the video below, when I tap on an item in the list, the wrong item is loaded (there's an offset between the row I click and the one that gets highlighted and loaded).
I also get the error SwiftUI encountered an issue when pushing aNavigationLink. Please file a bug.
Here's all my code:
import SwiftUI
struct ContentView: View {
#State var rootIsActive:Bool = false
var body: some View {
NavigationView{
AllProjectView(rootIsActive: self.rootIsActive)
}
.navigationBarTitle("Root")
.navigationViewStyle(StackNavigationViewStyle())
.environment(\.rootPresentationMode, self.$rootIsActive)
}
}
struct AllProjectView: View {
#State var rootIsActive:Bool = false
#State var projects: [String] = ["1", "2", "3"]
var body: some View{
List{
ForEach(projects.indices, id: \.self){ idx in
ProjectItem(name: self.$projects[idx], rootIsActive: self.$rootIsActive)
}
}.navigationBarTitle("All Projects")
}
}
struct ProjectItem: View{
#Binding var name: String
#Binding var rootIsActive: Bool
init(name: Binding<String>, rootIsActive: Binding<Bool>){
self._name = name
self._rootIsActive = rootIsActive
}
var body: some View{
NavigationLink(
destination: ProjectView(name: self.name),
isActive: self.$rootIsActive){
Text(name)
}
.isDetailLink(false)
.padding()
}
}
struct ProjectView: View {
var name: String
#State var isShowingSheet: Bool = false
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
#Environment(\.rootPresentationMode) private var rootPresentationMode: Binding<RootPresentationMode>
var body: some View{
VStack{
Text(name)
Button("Show Sheet"){
self.isShowingSheet = true
}
}
.sheet(isPresented: $isShowingSheet){
Button("return to root"){
self.isShowingSheet = false
print("pop view")
self.presentationMode.wrappedValue.dismiss()
print("pop root")
self.rootPresentationMode.wrappedValue.dismiss()
}
}
.navigationBarTitle("Project View")
}
}
// from https://stackoverflow.com/a/61926030/1720985
struct RootPresentationModeKey: EnvironmentKey {
static let defaultValue: Binding<RootPresentationMode> = .constant(RootPresentationMode())
}
extension EnvironmentValues {
var rootPresentationMode: Binding<RootPresentationMode> {
get { return self[RootPresentationModeKey.self] }
set { self[RootPresentationModeKey.self] = newValue }
}
}
typealias RootPresentationMode = Bool
extension RootPresentationMode {
public mutating func dismiss() {
self.toggle()
}
}
You only have one isRootActive variable that you're using. And, it's getting repeated for each item on the list. So, as soon as any item on the list is tapped, the isActive property for each NavigationLink turns to true.
Beyond that, your isRootActive isn't actually doing anything right now, since your "Return to root" button already does this:
self.isShowingSheet = false
self.presentationMode.wrappedValue.dismiss()
At that point, there's nothing more to dismiss -- it's already back at the root view.
My removing all of the root and isActive stuff, you get this:
struct ContentView: View {
var body: some View {
NavigationView{
AllProjectView()
}
.navigationBarTitle("Root")
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct AllProjectView: View {
#State var projects: [String] = ["1", "2", "3"]
var body: some View{
List{
ForEach(projects.indices, id: \.self){ idx in
ProjectItem(name: self.$projects[idx])
}
}.navigationBarTitle("All Projects")
}
}
struct ProjectItem: View{
#Binding var name: String
var body: some View{
NavigationLink(
destination: ProjectView(name: self.name)
){
Text(name)
}
.isDetailLink(false)
.padding()
}
}
struct ProjectView: View {
var name: String
#State var isShowingSheet: Bool = false
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var body: some View{
VStack{
Text(name)
Button("Show Sheet"){
self.isShowingSheet = true
}
}
.sheet(isPresented: $isShowingSheet){
Button("return to root"){
self.isShowingSheet = false
print("pop view")
self.presentationMode.wrappedValue.dismiss()
}
}
.navigationBarTitle("Project View")
}
}
If you had an additional view in the stack, you would need a way to keep track of if the root were active. I've used a custom binding here that converts an optional String representing the project's name to a Bool value that gets passed down the view hierarchy:
struct ContentView: View {
var body: some View {
NavigationView{
AllProjectView()
}
.navigationBarTitle("Root")
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct AllProjectView: View {
#State var projects: [String] = ["1", "2", "3"]
#State var activeProject : String?
func activeBindingForProject(name : String) -> Binding<Bool> {
.init {
name == activeProject
} set: { newValue in
activeProject = newValue ? name : nil
}
}
var body: some View{
List{
ForEach(projects.indices, id: \.self){ idx in
InterimProjectView(name: self.$projects[idx],
isActive: activeBindingForProject(name: self.projects[idx]))
}
}.navigationBarTitle("All Projects")
}
}
struct InterimProjectView: View {
#Binding var name : String
#Binding var isActive : Bool
var body : some View {
NavigationLink(destination: ProjectItem(name: $name, isActive: $isActive),
isActive: $isActive) {
Text("Next : \(isActive ? "true" : "false")")
}
}
}
struct ProjectItem: View {
#Binding var name: String
#Binding var isActive: Bool
var body: some View{
NavigationLink(
destination: ProjectView(name: self.name, isActive: $isActive)
){
Text(name)
}
.isDetailLink(false)
.padding()
}
}
struct ProjectView: View {
var name: String
#Binding var isActive : Bool
#State var isShowingSheet: Bool = false
var body: some View{
VStack{
Text(name)
Button("Show Sheet"){
self.isShowingSheet = true
}
}
.sheet(isPresented: $isShowingSheet){
Button("return to root"){
self.isShowingSheet = false
print("pop root")
self.isActive.toggle()
}
}
.navigationBarTitle("Project View")
}
}

How to dismiss sheet from within NavigationLink

In the following example, I have a view that shows a sheet of ViewOne. ViewOne has a NavigationLink to ViewTwo.
How can I dismiss the sheet from ViewTwo?
Using presentationMode.wrappedValue.dismiss() navigates back to ViewOne.
struct ContentView: View {
#State private var isShowingSheet = false
var body: some View {
Button("Show sheet", action: {
isShowingSheet.toggle()
})
.sheet(isPresented: $isShowingSheet, content: {
ViewOne()
})
}
}
struct ViewOne: View {
var body: some View {
NavigationView {
NavigationLink("Go to ViewTwo", destination: ViewTwo())
.isDetailLink(false)
}
}
}
struct ViewTwo: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Dismiss sheet here") {
presentationMode.wrappedValue.dismiss()
}
}
}
This may depend some on platform -- in a NavigationView on macOS, for example, your existing code works.
Explicitly passing a Binding to the original sheet state should work:
struct ContentView: View {
#State private var isShowingSheet = false
var body: some View {
Button("Show sheet", action: {
isShowingSheet.toggle()
})
.sheet(isPresented: $isShowingSheet, content: {
ViewOne(showParentSheet: $isShowingSheet)
})
}
}
struct ViewOne: View {
#Binding var showParentSheet : Bool
var body: some View {
NavigationView {
NavigationLink("Go to ViewTwo", destination: ViewTwo(showParentSheet: $showParentSheet))
//.isDetailLink(false)
}
}
}
struct ViewTwo: View {
#Binding var showParentSheet : Bool
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Dismiss sheet here") {
showParentSheet = false
}
}
}

NavigationLink dismisses after TextField changes

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()
}
}
}