NavigationStack not affected by EnvironmentObject changes - swiftui

I'm attempting to use #EnvironmentObject to pass an #Published navigation path into a SwiftUI NavigationStack using a simple wrapper ObservableObject, and the code builds without issue, but working with the #EnvironmentObject has no effect. Here's a simplified example that still exhibits the issue:
import SwiftUI
class NavigationCoordinator: ObservableObject {
#Published var path = NavigationPath()
func popToRoot() {
path.removeLast(path.count)
}
}
struct ContentView: View {
#StateObject var navigationCoordinator = NavigationCoordinator()
var body: some View {
NavigationStack(path: $navigationCoordinator.path, root: {
FirstView()
})
.environmentObject(navigationCoordinator)
}
}
struct FirstView: View {
var body: some View {
VStack {
NavigationLink(destination: SecondView()) {
Text("Go To SecondView")
}
}
.navigationTitle(Text("FirstView"))
}
}
struct SecondView: View {
var body: some View {
VStack {
NavigationLink(destination: ThirdView()) {
Text("Go To ThirdView")
}
}
.navigationTitle(Text("SecondView"))
}
}
struct ThirdView: View {
#EnvironmentObject var navigationCoordinator: NavigationCoordinator
var body: some View {
VStack {
Button("Pop to FirstView") {
navigationCoordinator.popToRoot()
}
}
.navigationTitle(Text("ThirdView"))
}
}
I am:
Passing the path into the NavigationStack path parameter
Sending the simple ObservableObject instance into the NavigationStack via the .environmentObject() modifier
Pushing a few simple child views onto the stack
Attempting to use the environment object in ThirdView
NOT crashing when attempting to use the environment object (e.g. "No ObservableObject of type NavigationCoordinator found")
Am I missing anything else that would prevent the deeply stacked view from using the EnvironmentObject to affect the NavigationStack's path? It seems like the NavigationStack just isn't respecting the bound path.
(iOS 16.0, Xcode 14.0)

The reason your code is not working is that you haven't added anything to your path, so your path is empty. You can simply verify this by adding print(path.count) in your popToRoot method it will print 0 in the console.
To work with NavigationPath you need to use navigationDestination(for:destination:) ViewModifier, So for your example, you can try something like this.
ContentView:- Change NavigationStack like this.
NavigationStack(path: $navigationCoordinator.path) {
VStack {
NavigationLink(value: 1) {
Text("Go To SecondView")
}
}
.navigationDestination(for: Int.self) { i in
if i == 1 {
SecondView()
}
else {
ThirdView()
}
}
}
SecondView:- Change NavigationLink like this.
NavigationLink(value: 2) {
Text("Go To ThirdView")
}
This workaround works with Int but is not a better approach, so my suggestion is to use a custom Array as a path. Like this.
enum AppView {
case second, third
}
class NavigationCoordinator: ObservableObject {
#Published var path = [AppView]()
}
NavigationStack(path: $navigationCoordinator.path) {
FirstView()
.navigationDestination(for: AppView.self) { path in
switch path {
case .second: SecondView()
case .third: ThirdView()
}
}
}
Now change NavigationLink in FirstView and SecondView like this.
NavigationLink(value: AppView.second) {
Text("Go To SecondView")
}
NavigationLink(value: AppView.third) {
Text("Go To ThirdView")
}
The benefit of the above is now you can use the button as well to push a new screen and just need to append in your path.
path.append(.second)
//OR
path.append(.third)
This will push a respected view.
For more details, you can read the Apple document of NavigationLink and NavigationPath.

Related

Non-deprecated way to call NavigationLink on Buttons

This is the old way of calling NavigationLink on Buttons
struct ContentView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: View1(), tag: "tag1", selection: $selection) {
EmptyView()
}
NavigationLink(destination: NotView1(), tag: "tag2", selection: $selection) {
EmptyView()
}
Button("Do work then go to View1") {
// do some work that takes about 1 second
mySleepFunctionToSleepOneSecond()
selection = "tag1"
}
Button("Instantly go to NotView1") {
selection = "tag2"
}
}
.navigationTitle("Navigation")
}
}
}
This code works perfectly. It can go to different View targets depending on which button is clicked. Not only that, it guarantees all work is done BEFORE navigating to the target view. However, the only issue is that 'init(destination:tag:selection:label:)' was deprecated in iOS 16.0: use NavigationLink(value:label:) inside a List within a NavigationStack or NavigationSplitView
I get NavigationStack is awesome and such. But how can I translate the code to use the new NavigationStack + NavigationLink. Especially, how can I make sure work is done Before navigation?
Using new NavigationStack and its path property you can do much more. Your example will be transformed to
struct ContentView: View {
#State private var path = [String]()
var body: some View {
NavigationStack(path: $path) {
VStack {
Button("Do work then go to View1") {
// do some work that takes about 1 second
mySleepFunctionToSleepOneSecond()
path.append("tag1")
}
Button("Instantly go to NotView1") {
path.append("tag2")
}
}
.navigationTitle("Navigation")
.navigationDestination(for: String.self) { route in
switch route {
case "tag1":
EmptyView()
case "tag2":
EmptyView()
default:
EmptyView()
}
}
}
}
}
Check this video. There you can find more use cases.
For using non deprecated and after doing some work if we want to go to next view or in anyview there is something called ".navigationDestination". Let's see that using simple example.
#State var bool : Bool = false
var body: some View {
NavigationStack {
VStack {
Text("Hello, world!")
Button {
//Code here before changing the bool value
bool = true
} label: {
Text("Navigate Button")
}
}.navigationDestination(isPresented: $bool) {
SwiftUIView()
}
}
}
In this code we change take bool value as false and change it to true when our work is done using button.
.navigationDestination(isPresented: Binding<Bool>, destination: () -> View)
In .navigationDestination pass the Binding bool and provide the view you want to navigate.
You can use .navigationDestination multiple times.
Hope you found this useful.

SwiftUI - TabView/NavigationLink navigation breaks when using a custom binding

I'm having trouble with what I think may be a bug, but most likely me doing something wrong.
I have a slightly complex navigation state variable in my model that I'm using for tracking/setting state between tab and sidebar presentations when multitasking on iPad. That all works fine except in tab mode, once I use a navigation link once I can't seem to use one again, whether the binding is on my tab view or navigation links in a list.
Would really appreciate any thoughts on this,
Cheers!
Example
NavigationItem.swift
enum SubNavigationItem: Hashable {
case overview, user, hobby
}
enum NavigationItem: Hashable {
case home(SubNavigationItem)
case settings
}
Model.swift
final class Model: ObservableObject {
#Published var selectedTab: NavigationItem = .home(.overview)
}
SwiftUIApp.swift
#main
struct SwiftUIApp: App {
#StateObject var model = Model()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(model)
}
}
}
ContentView.swift
struct ContentView: View {
var body: some View {
AppTabNavigation()
}
}
AppTabNavigation.swift
struct AppTabNavigation: View {
#EnvironmentObject private var model: Model
var body: some View {
TabView(selection: $model.selectedTab) {
NavigationView {
HomeView()
}
.tabItem {
Label("Home", systemImage: "house")
}
.tag(NavigationItem.home(.overview))
NavigationView {
Text("Settings View")
}
.tabItem {
Label("Settings", systemImage: "gear")
}
.tag(NavigationItem.settings)
}
}
}
HomeView.swift
I created a binding here because selection required an optional <NavigationItem?> not
struct HomeView: View {
#EnvironmentObject private var model: Model
var body: some View {
let binding = Binding<NavigationItem?>(
get: {
model.selectedTab
},
set: {
guard let item = $0 else { return }
model.selectedTab = item
}
)
List {
NavigationLink(
destination: Text("Users"),
tag: .home(.user),
selection: binding
) {
Text("Users")
}
NavigationLink(
destination: Text("Hobbies"),
tag: .home(.hobby),
selection: binding
) {
Text("Hobbies")
}
}
.navigationTitle("Home")
}
}
Second Attempt
I tried making the selectedTab property optional as #Lorem Ipsum suggested. Which means I can remove the binding there. But then the TabView doesn't work with the property. So I create a binding for that and have the same issue but with the tab bar!
Make the selected tab optional
#Published var selectedTab: NavigationItem? = .home(.overview)
And get rid of that makeshift binding variable. Just use the variable
$model.selectedTab
If the variable can never be nil then something is always selected IAW with that makeshift variable it will just keep the last value.

PresentationMode.dismiss weird behaviour when using multiple NavigationLinks inside ForEach

My app has 4 views (let's call them View_A[root] -> View_B -> View_C -> View_D). The navigation between them was made using NavigationView/NavigationLink.
When I call self.presentationMode.wrappedValue.dismiss() from the last view(View_D) I expect it to dismiss the current view (D) only, but for some reason it dismissed ALL the views and stops at view A (root view).
That's weird.
I spent a couple of hours trying to figure out what's going on there and I found that
- if I remove "ForEach" from "View_A" it works correctly and only the last view is dismissed. Even though ForEach gets just 1 static object in this example.
The second weird thing is that
- if I don't change "self.thisSession.stats" to false it also works correctly dismissing only the last view.
This is super weird as View_A (as far as I understand) is not dependent on thisSession environment variable.
Any ideas on how to prevent View_C and View_B from being dismissed in this case? I wanna end up at View_C after clicking the link, not at View_A.
Any help is appreciated, it took me a while to find out where it comes from but I'm not smart enough to proceed any further ;)
import SwiftUI
struct A_View: View {
#EnvironmentObject var thisSession: CurrentSession
var body: some View {
NavigationView {
VStack {
Text("View A")
ForEach([TestObject()], id: \.id) { _ in
NavigationLink(destination: View_B() ) {
Text("Move to View B")
}
}
}
}
}
}
struct View_B: View {
var body: some View {
NavigationView {
NavigationLink(destination: View_C()
) {
Text("GO TO VIEW C")
}
}
}
}
struct View_C: View {
var body: some View {
ZStack {
NavigationView {
NavigationLink(destination: View_D()) {
Text("GO TO VIEW D")
}
}
}
}
}
struct View_D: View {
#EnvironmentObject var thisSession: CurrentSession
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
ZStack {
VStack {
Button(action: {
self.thisSession.stats = false
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Return!")
}
}
}
}
}
class CurrentSession: ObservableObject {
#Published var stats: Bool = false
#Published var user: String = "user"
}
struct TestObject: Identifiable, Codable {
let id = UUID()
}
Your issue is with:
NavigationView
There is only supposed to be one NavigationView in an entire view stack. Try removing the NavigationView from views B and C

Updating EnvironmentObject pushed me back

I have two views ListView and DetailView
ListView:
#EnvironmentObject var userData: UserData
var body: some View {
VStack {
ForEach(userData.packs) { pack in
if pack.added {
NavigationLink(destination: DetailView(packIndex: self.userData.packs.firstIndex(where: { $0.id == pack.id })!)) {
MyRowViewDoesntMatter(pack: pack)
}
}
}
}
.padding(.horizontal)
}
DetailView:
#EnvironmentObject var userData: UserData
var packIndex: Int
VStack {
List {
VStack {
.... some Vies ... doesn't matter
.navigationBarItems(trailing:
THE PROBLEM IS HERE (BELOW)
Button(action: {
self.userData.packs[self.packIndex].added.toggle()
}) {
Image(systemName: self.userData.packs[self.packIndex].added ? "plus.circle.fill" : "plus.circle")
}
...
The problem is when I click on button in the navigationBarItems in DetailView. The "added" property of the "#EnvironmentObject var userData: UserData" is updated and the user's screen is going back (to the RowView). I fond out that the problem with EnvironmentObject, because the data is updated and View tries to rerender (?) that is why it pushes me back?
How to fix it? I want to stay at the DetailView screen after clicking the button.
P.S. I need to use EnvironmentObject type because then when I go back I need to see the results.
Thank you very much!
Here is possible approach (by introducing some kind of selection). As NavigationView does not allow to remove link from stack (as identifier of stacked navigation), probably also worth considering separate view model for DetailView to be applied into common container on finish editing.
Tested with Xcode 11.4 / iOS 13.4.
Some replication of your code, used for testing:
struct ListView: View {
#EnvironmentObject var userData: PushBackUserData
#State private var selectedPack: Pack? = nil
var body: some View {
NavigationView {
VStack {
ForEach(Array(userData.packs.enumerated()), id: \.element.id) { i, pack in
NavigationLink("Pack \(pack.id)", destination:
DetailView(pack: self.$selectedPack)
.onAppear {
self.selectedPack = pack
}
.onDisappear {
self.userData.packs[i].added = self.selectedPack?.added ?? false
}
).isHidden(!pack.added)
}
}
.padding(.horizontal)
}
}
}
struct DetailView: View {
#Binding var pack: Pack?
var body: some View {
VStack {
List {
VStack {
Text("Pack \(pack?.id ?? "<none>")")
}
}
.navigationBarItems(trailing:
Button(action: {
self.pack?.added.toggle()
}) {
Image(systemName: pack?.added ?? false ? "plus.circle.fill" : "plus.circle")
}
)
}
}
}
just convenient helper extension
extension View {
func isHidden(_ hidden: Bool) -> some View {
Group {
if hidden { self.hidden() }
else { self }
}
}
}

How to disable NavigationView push and pop animations

Given this simple NavigationView:
struct ContentView : View {
var body: some View {
NavigationView {
VStack {
NavigationLink("Push Me", destination: Text("PUSHED VIEW"))
}
}
}
}
Did anyone find a way of disabling the NavigationView animation when a destination view is pushed/popped into/from the stack?
This has been possible in UIKit since iOS2.0! I think it is not too much to ask from the framework. I tried all sorts of modifiers on all views (i.e., the NavigationView container, the destination view, the NavigationLink, etc)
These are some of the modifiers I tried:
.animation(nil)
.transition(.identity)
.transaction { t in t.disablesAnimations = true }
.transaction { t in t.animation = nil }
None made a difference. I did not find anything useful in the EnvironmentValues either :-(
Am I missing something very obvious, or is the functionality just not there yet?
Xcode 11.3:
Right now there is no modifier to disable NavigationView animations.
You can use your struct init() to disable animations, as below:
struct ContentView : View {
init(){
UINavigationBar.setAnimationsEnabled(false)
}
var body: some View {
NavigationView {
VStack {
NavigationLink("Push Me", destination: Text("PUSHED VIEW"))
}
}
}
}
First you need state for the NavigationLink to respond to, then set that state inside a transaction with animations disabled, as follows:
struct ContentView : View {
#State var isActive = false
var body: some View {
NavigationView {
VStack {
NavigationLink(isActive: $isActive, destination: {
Text("PUSHED VIEW")}) {
Text("Push Me")
}
Button("Navigate Without Animation") {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
isActive = true
}
}
}
}
}
}
I recently created an open source project called swiftui-navigation-stack (https://github.com/biobeats/swiftui-navigation-stack) that contains the NavigationStackView, a view that mimics the navigation behaviours of the standard NavigationView adding some useful features. For example, you could use the NavigationStackView and disable the transition animations as requested by Kontiki in the question. When you create the NavigationStackView just specify .none as transitionType:
struct ContentView : View {
var body: some View {
NavigationStackView(transitionType: .none) {
ZStack {
Color.yellow.edgesIgnoringSafeArea(.all)
PushView(destination: View2()) {
Text("PUSH")
}
}
}
}
}
struct View2: View {
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.all)
PopView {
Text("POP")
}
}
}
}
PushView and PopView are two views that allow you push and pop views (similar to the SwiftUI NavigationLink). Here is the complete example:
import SwiftUI
import NavigationStack
struct ContentView : View {
var body: some View {
NavigationStackView(transitionType: .none) {
ZStack {
Color.yellow.edgesIgnoringSafeArea(.all)
PushView(destination: View2()) {
Text("PUSH")
}
}
}
}
}
struct View2: View {
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.all)
PopView {
Text("POP")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The result is:
It would be great if you guys joined me in improving this open source project.