I want to have a root NavigationStack that lets the user navigate around a SwiftUI app, including to a TabView with tabs that have their own navigation stack. Unfortunately, this seems to not work at all (xcode 14.2, iOS 16).
The following example demonstrates the issue. When you attempt to navigate inside the tab view navigation stack, the tabs disappear and then the app goes into a broken state where navigation basically stops working entirely.
import SwiftUI
struct TabsView: View {
var body: some View {
TabView {
NavigationStack {
ZStack {
NavigationLink("Navigate to child tab", value: 1)
}
.navigationDestination(for: Int.self) { screen in
Text("Tab child \(screen)")
}
}
.tabItem {
Label("Screen 1", systemImage: "house")
}
Text("Screen 2")
.tabItem {
Label("Screen 2", systemImage: "house")
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationStack {
VStack {
NavigationLink("Show Tabs", value: "tabs")
}
.navigationDestination(for: String.self) { screen in
if screen == "tabs" {
TabsView()
} else {
Text("?")
}
}
}
}
}
How can I make this work?
Use NavigationView in ContentView. Apple has problems with NavigationStack in hierarchy of NavigationStack.
Related
I'm not sure if Xcode has a bug or if I'm wrong, but I have mi Home() view which has a TabView, this tab view allow me to navigate to differences views, while I was coding my SettingsView() I notice than in canvas I can see the NavigationBarTitle, but then I run simulator, the title don't appear.
Home() View
struct Home: View {
#State var tabPredeterminado:Int = 1
var body: some View {
TabView(selection: $tabPredeterminado) {
SettingsView()
.tabItem{
Image(systemName: "gearshape.fill")
Text("Settings")
}.tag(3)
}
.navigationBarBackButtonHidden(true)
}
}
SettingsView()
var body: some View {
NavigationStack {
List {...}
.navigationBarTitle("Settings")
}
}
}
Simulator
In one of the projects I decided to comply with apple and get rid of NavigationView.
In the project you had a list of buildings and after choosing a Building you had several tabs with bills, documents etc. Each of the Tabs was a different view, each with its own toolbar, buttons and functions. Here's the sample code:
import SwiftUI
struct TestView: View {
var testData: [Building] = [
Building(name: "Crystal Balls"),
Building(name: "Geneva Towers"),
Building(name: "Villa Navagero"),
]
var body: some View {
//Here's the problem... If I change it to NavigationStack
//all the of toolbars in tabs are gone
NavigationView {
List(testData) {test in
NavigationLink {
TabView {
NavigationStack{
Text(test.name)
.navigationTitle("My Bills")
.navigationBarTitleDisplayMode(.inline)
.toolbar{
ToolbarItem{
Button{} label: {
Image(systemName: "gear.circle")
}
}
ToolbarItem{
Button{} label: {
Image(systemName: "plus.circle")
}
}
}
}
.tabItem{
Label("Bills", systemImage: "doc.fill.badge.ellipsis")
}
Text(test.name)
.tabItem{
Label("Skills", systemImage: "doc.fill")
}
}
} label: {
Text(test.name)
}
}
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
Having every tab as a different view and having a back button is too convenient, getting all the toolbar button to the TabView seems to "dirty" and inefficient.
Maybe somebody has figured out a solution?
When you navigate and open the confirmation dialog. When you select Yes, No or Cancel the page that the app was on is dismissed and it takes you back to the form on the previous page.
We also found this happens with alerts too.
It's a simple enough app structure, top level tabs then a menu which links to sub pages.
Here is a quick demo of the bug:
We put together an example app that demonstrates this.
How can we prevent this from happening while also maintaining the app structure?
import SwiftUI
#main
struct testApp: App {
var body: some Scene {
WindowGroup {
NavigationView {
TabView() {
Form {
NavigationLink(destination: SubPage()) {
Image(systemName: "clock")
Text("Sub Page")
}
// This is where more menu options would be
}
.tag(1)
.tabItem {
Image(systemName: "square.grid.2x2")
Text("Tab 1")
}
// This is where more tab pages would be
}
}
}
}
}
struct SubPage: View {
#State private var confirmDialogVisible = false
var body: some View {
VStack {
Button{
confirmDialogVisible = true
} label: {
Text("popup")
}
}
.confirmationDialog("Confirm?", isPresented: $confirmDialogVisible) {
Button("Yes") {
print("yes")
}
Button("No", role: .destructive) {
print("no")
}
}
}
}
We are using XCode 14.1
And running on iOS 16.1
I usually use ViewModifer to keep consistency between tabs. One modifier for the root of the tab and one for the children.
///Modifier that uses `ToolbarViewModifier` and includes a `NavigationView`
struct NavigationViewModifier: ViewModifier{
func body(content: Content) -> some View {
NavigationView{
content
.modifier(ToolbarViewModifier())
}
}
}
///`toolbar` that can be used by the root view of the navigation
///and the children of the navigation
struct ToolbarViewModifier: ViewModifier{
let title: String = "Company Name"
func body(content: Content) -> some View {
content
.toolbar {
ToolbarItem(placement: .principal) {
VStack{
Image(systemName: "sparkles")
Text(title)
}
}
}
}
}
Then the Views use it something like this.
import SwiftUI
struct CustomTabView: View {
var body: some View {
TabView{
ForEach(0..<4){ n in
CustomChildView(title: n.description)
//Each tab gets a `NavigationView` and the shared toolbar
.modifier(NavigationViewModifier())
.tabItem {
Text(n.description)
}
}
}
}
}
struct CustomChildView: View {
let title: String
#State private var showConfirmation: Bool = false
var body: some View {
VStack{
Text(title)
NavigationLink {
CustomChildView(title: "\(title) :: \(UUID().uuidString)")
} label: {
Text("open child")
}
Button("show confirmation") {
showConfirmation.toggle()
}
.confirmationDialog("Confirm?", isPresented: $showConfirmation) {
Button("Yes") {
print("yes")
}
Button("No", role: .destructive) {
print("no")
}
}
}
//Each child uses the shared toolbar
.modifier(ToolbarViewModifier())
}
}
I stuck with NavigationView since that is what you have in your code but
if we take into consideration the new NavigationStack the possibilities of these two modifiers become exponentially better.
You can include custom back buttons that only appear if the path is not empty, return to the root from anywhere, etc.
Apple says that
Tab bars use bar items to navigate between mutually exclusive panes of content in the same view
Make sure the tab bar is visible when people navigate to different areas in your app
https://developer.apple.com/design/human-interface-guidelines/components/navigation-and-search/tab-bars/
Having the NavigationView or NavigationStack on top goes against these guidelines and therefore are the source of endless bugs, Especially when you take into consideration iPadOS.
Simple solution would be to use navigationDestination.
struct testApp: App {
#State var goToSubPage = false
var body: some Scene {
WindowGroup {
NavigationStack {
TabView() {
Form {
VStack {
Image(systemName: "clock")
Text("Sub Page")
}
.onTapGesture {
goToSubPage = true
}
// This is where more menu options would be
}
.navigationDestination(isPresented: $goToSubPage, destination: {
SubPage()
})
.tag(1)
.tabItem {
Image(systemName: "square.grid.2x2")
Text("Tab 1")
}
// This is where more tab pages would be
}
}
}
}
}
I tested it and it won't popped off itself anymore.
I want to place a TabView inside a NavigationView with different titles depending on the selected tab. Inside those tabs I want to place a List view. See the code below:
struct ContentView: View {
#State private var selection = 1
var body: some View {
TabView(selection:$selection) {
Page_1()
.tabItem {
Image(systemName: "book")
Text("Page 1")
}
.tag(1)
Page_2()
.tabItem {
Image(systemName: "calendar")
Text("Page 2")
}
.tag(2)
}
}
}
struct Page_1: View {
#State var selectedTab = "1"
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
List {
ForEach(0..<20){i in
Text("Test")
}
}
.tag("1")
.navigationBarTitle("Page 1 Tab 1")
List {
ForEach(0..<20){i in
Text("Test")
}
}
.tag("2")
.navigationBarTitle("Page 1 Tab 2")
}
.tabViewStyle(.page(indexDisplayMode: .never))
.ignoresSafeArea(.all)
.background()
}
}
}
struct Page_2: View {
#State var selectedTab = "1"
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
List {
ForEach(0..<20){i in
Text("Test")
}
}
.tag("1")
.navigationBarTitle("Page 2 Tab 1")
List {
ForEach(0..<20){i in
Text("Test")
}
}
.tag("2")
.navigationBarTitle("Page 2 Tab 2")
}
.tabViewStyle(.page)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.ignoresSafeArea()
.background()
}
}
}
The problem is that when the Pages first appear the lists inside their TabViews seem to be placed slightly too low and then move up. You can see this especially when you switch tabs like here:
After switching back and forth between the tabs they are placed correctly until I freshly start the app again. Would really appreciate your help!:)
Edit
As suggested I tried to put the NavigationViews inside the TabView. That solves the problem with the wrong positioning. However, it leads to the views not being shown at all before I switch back and forth between them. You can see what that looks like in the picture below:
I'm just picking up SwiftUI after a long break but I don't understand why I can't place a Navigation View within a Tab View.
I want my Navigation View to be a .tabItem so the view appears as part of the main app navigation so I'm trying this :
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
.tabItem {
Text("Home")
}
Text("Tab bar test")
.navigationBarTitle("Page One")
}
}
This doesn't work with error message
Cannot infer contextual base in reference to member 'tabItem'
But when I try this
var body: some View {
TabView {
NavigationView {
Text("Tab bar test")
.navigationBarTitle("Page One")
}
}
.tabItem {
Image(systemName: "1.circle")
Text("Home")
}
}
It builds fine but the tab bar doesn't show up.
My primary question which I'm hoping would be useful to others, is ...
Why can't I make a make a Navigation View a tab bar item by nesting .tabItem directly inside the Navigation View (as per my first example)?
I think it's a similar question to this one but there's no code there. And then quite similar to this one but they seem to be using .tabItem directy with ContentView like I want to with NavigationView but that isn't building!?
I'm probably misunderstanding something simple but I don't get this at all at the moment.
Do this for a better overview:
ContentView:
struct ContentView : View {
var body: some View {
TabView {
FirstView()
.tabItem {
Image(systemName: "folder.fill")
Text("Home")
}
SecondView()
.tabItem {
Image(systemName: "folder.fill")
Text("SecondView")
}
}
}
}
FirstView
struct FirstView: View {
var body: some View {
NavigationView {
Text("FirstView")
.navigationBarTitle("Home")
}
}
}
}
SecondView
struct SecondView: View {
var body: some View {
NavigationView {
Text("SecondView")
.navigationBarTitle("Home")
}
}
}
}
.tabItem should be used as a modifier on the view that represents that tab.
In the first example, this doesn't work because of a syntax error -- you're trying to use it on the opening of a closure in NavigationView {, when instead you want it on the outside of the closing brace: NavigationView { }.tabItem(...)
In the second example, you're using .tabItem as a modifier on the entire TabView instead of the NavigationView.
Both of your examples may have revealed what was going on more obviously if you indent your code so that you can see the hierarchy. Trying selecting your code in Xcode and using Ctrl-I to get Xcode to properly format it for you.
Here's a working version:
struct ContentView : View {
var body: some View {
TabView {
NavigationView {
Text("Tab bar test")
.navigationBarTitle("Page One")
}
.tabItem { //note how this is modifying `NavigationView`
Image(systemName: "1.circle")
Text("Home")
}
}
}
}