I have a bug in my application so I decided to create a self-contained example for the sake of being simple, which reproduces the problem.
I have a content view with two tabs: contacts and users:
struct ContentView: View {
@StateObject var navigationState = NavigationState()
var body: some View {
TabView(selection: $navigationState.tabSelection) {
ContactsView()
.tabItem {
Image(systemName: "star")
}
.tag(NavigationState.TabSelection.contacts)
.environmentObject(navigationState)
NavigationStack(path: $navigationState.users) {
UserListView()
}.tabItem {
Image(systemName: "person")
}
.tag(NavigationState.TabSelection.users)
.environmentObject(navigationState)
}
}
}
NavigationState holds the state of the selected tab and navigation:
class NavigationState: ObservableObject {
enum TabSelection {
case contacts, users
}
@Published var users: [User] = []
@Published var tabSelection: TabSelection = .contacts
}
Navigation is only possible in the 'users' tab, while the 'contacts' tab holds a list of usernames. Once the user clicks on a username, it should navigate on the other tab (users) and show that username:
struct ContactsView: View {
@EnvironmentObject var navigationState: NavigationState
@State var users: [User] = [
User(username: "A"),
User(username: "D")
]
var body: some View {
VStack {
ForEach(users) { user in
Text(user.username)
.onTapGesture {
navigationState.tabSelection = .users
navigationState.users = [user]
}
}
}
}
}
Instead, from the 'users' view, I first display a list of usernames, and then if I click on any of them I should see that username displayed:
struct UserListView: View {
@EnvironmentObject var navigationState: NavigationState
@State var users = [
User(username: "A"),
User(username: "B"),
User(username: "C")
]
var body: some View {
VStack {
ForEach(users) { user in
Text(user.username)
.onTapGesture {
navigationState.users = [user]
}
}
}
.navigationDestination(for: User.self) { user in
UserView(user: user)
}
}
}
The detail view for a user is here:
struct UserView: View {
@State var user: User
var body: some View {
VStack {
Text(user.username)
}
}
}
So to summarize, when I navigate to the 'users' tab, this is what I see:
If I click on 'B', a UserView object is pushed in the navigation stack and this is what I see:
So far so good. But what if I want to navigate to the 'users' tab and show another username ('D') in my example from the 'contacts' tab? This is what I see when I open the 'contacts' tab for the first time:
If I click on 'D', this code should be executed:
Text(user.username)
.onTapGesture {
navigationState.tabSelection = .users
navigationState.users = [user]
}
So I would expect the 'users' tab to be selected, with user 'D' shown. Instead this is what I see:
I still display user 'A', which was my previous selection. This doesn't happen if in the 'users' tab I don't navigate to any user. So for example, if I run these steps:
In this case I correctly display 'D' in the 'users' tab. The problem occurs only when I had previously navigated to another user from the 'users' tab.
Since my NavigationState object is @StateObject and it's passed down the hierarchy to every subview, I would expect every view to have the correct version of my navigation state. I verified this by printing the variable and indeed it's updated correctly. Does anybody know if there is a solution (which probably involves using different modifiers) to go around this problem?
This seems to be a side effect of a formerly initialized @State var that has not been declared private (which one should always do :))
In UserView
replace
@State var user: User
with
let user: User
If I do that it runs fine for me.