I've been trying to figure out why my navigation is broken in iOS 16.4 It worked in 16.3. I have been able to fix the problem, however, I would like to know why this fixes it. I don't understand.
The problem I was trying to fix is this:
Whenever I appended an item to the NavigationPath, the new view never got pushed (it did in iOS 16 - 16.3). Nothing happened in the view. Then, when I tap on a normal navigation link as in NavigationLink(value: Destinations.goal),
the previously appended path would push, followed by a push of the view specified for the NavigationLink's value in .navigationDestination(for:).
In MainGoalView
, the line navItem.navigationPath.append(Destinations.formStatus)
had no effect when I tap on "Submitted Forms". However, if I tapped "Submitted Forms", then tap on "Information", both links get executed so that I see the view "Information". Then tapping the Back button, I see "Form Status". (I also saw "Form Status" momentarily before "Information" was pushed.)
struct MainGoalView: View {
@EnvironmentObject var navItem: AppNavigationItem
enum Destinations: Hashable, Codable { case formStatus, information }
@State private var isShowingAlert = false
@State private var isShowingForms = false
@State private var navigationPath = NavigationPath()
var body: some View {
List {
Section {
UpcomingGoalsView()
} header: {
Text("Upcoming")
}
Section {
Text("Form Summary")
} header: {
SectionActionHeader(title: "Submitted Forms", actionText: "View") {
navItem.navigationPath.append(Destinations.formStatus) // <- HERE
}
}
Section {
NavigationLink(value: Destinations.information) {
Text("Information")
}
} header: {
Text("Information")
}
}
.listStyle(.insetGrouped)
.navigationDestination(for: Destinations.self) { value in
switch value {
case .formStatus: Text("Form Status")
case .information: Text("Information")
}
}
}
}
I finally tracked the problem down to the code that sets up my tab bar. Before looking at that code, here is the code code that configures the tab bar. The AppNavigator
class contains a list of AppNavigationItem
s that defines each tab of the tab bar. (This was set up so it could be used for a tab bar, or for another interface such as a NavigationSplitView)
enum AppNavigatorType {
case insights
case people
}
class AppNavigator: ObservableObject {
static let shared = AppNavigator()
static let defaultStartTab = AppNavigatorType.insights
@Published var selectedNavigatorType: AppNavigatorType? = AppNavigator.defaultStartTab
var items: [AppNavigationItem] = [
AppNavigationItem(AnyView(InsightsView()), type: .insights, title: "Insights", systemImageName: "chart.bar"),
AppNavigationItem(AnyView(Text("Not Implemented")), type: .people, title: "People", systemImageName: "person"),
]
}
class AppNavigationItem: ObservableObject, Identifiable {
@Published var navigationPath = NavigationPath()
var id: String { title }
var view: AnyView
var type: AppNavigatorType?
var title: String
private var systemImageName: String
var image: Image
init(_ view: AnyView, type: AppNavigatorType, title: String, systemImageName: String) {
self.view = view
self.type = type
self.title = title
self.systemImageName = systemImageName
self.image = Image(systemName: systemImageName)
}
}
Finally, here is the code where I was able to correct the problem, but I'm not sure why it fixes it.
First, here is the version that doesn't work correctly. I was never quite sure as to why I had to use the $ to get it to compile, but it all worked in iOS 16 - 16.3
public struct MainTabView: View {
@EnvironmentObject var navigator: AppNavigator
public var body: some View {
TabView {
ForEach ($navigator.items) { $navItem in // <- Why the $
NavigationStack(path: $navItem.navigationPath) {
let _: Int = { print(navItem.navigationPath); return 0 }()
navItem.view
}
.environmentObject(navItem)
.tabItem {
Text(navItem.title)
navItem.image
.environment(\.symbolVariants, navigator.selectedNavigatorType == navItem.type ? .fill : .none)
}
.tag(navItem.type)
.navigationTitle(navItem.title)
.id(navItem.id)
}
}
}
}
Unfortunately, it doesn't work in iOS 16.4, as already described.
I changed the above code for MainTableView
to the following, and now it works in iOS 16.4. I just moved the code inside the ForEach
into another View.
I would love someone smarter than me to help me understand why.
public struct MainTabView: View {
@EnvironmentObject var navigator: AppNavigator
public var body: some View {
TabView {
ForEach (navigator.items) { navItem in // <- Ah! $ not needed now.
TabBarRootView(navItem: navItem)
}
}
}
}
struct TabBarRootView: View {
@EnvironmentObject private var navigator: AppNavigator
@ObservedObject var navItem: AppNavigationItem // <- navItem is an ObservableObject, and this is observing it.
var body: some View {
NavigationStack(path: $navItem.navigationPath) { // <- Pass the binding to the published navigationPath.
let _: Int = { print(navItem.navigationPath); return 0 }()
navItem.view
}
.environmentObject(navItem)
.tabItem {
Text(navItem.title)
navItem.image
.environment(\.symbolVariants, navigator.selectedNavigatorType == navItem.type ? .fill : .none)
}
.tag(navItem.type)
.navigationTitle(navItem.title)
.id(navItem.id)
}
}