I am new to SwiftUI (like most people) and trying to figure out how to remove some whitespace above a List that I embedded in a NavigationView.
In this image, you can see that there is some white space above the List.
What I want to accomplish is this:
I've tried using:
.navigationBarHidden(true)
but this did not make any noticeable changes.
I'm currently setting up my navigiationView like this:
NavigationView {
FileBrowserView(jsonFromCall: URLRetrieve(URLtoFetch: applicationDelegate.apiURL))
.navigationBarHidden(true)
}
where FileBrowserView is a view with a List and FileCells defined like this:
List {
Section(header: Text("Root")) {
FileCell(name: "Test", fileType: "JPG",fileDesc: "Test number 1")
FileCell(name: "Test 2", fileType: "txt",fileDesc: "Test number 2")
FileCell(name: "test3", fileType: "fasta", fileDesc: "")
}
}
I do want to note that the ultimate goal here is that you will be able to click on these cells to navigate deeper into a file tree and thus should display a Back button on the bar on deeper navigation, but I do not want anything at the top as such during my initial view.
For some reason, SwiftUI requires that you also set .navigationBarTitle for .navigationBarHidden to work properly.
NavigationView {
FileBrowserView(jsonFromCall: URLRetrieve(URLtoFetch: applicationDelegate.apiURL))
.navigationBarTitle("")
.navigationBarHidden(true)
}
Update
As #Peacemoon pointed out in the comments, the navigation bar remains hidden as you navigate deeper in the navigation stack, regardless of whether or not you set navigationBarHidden to false in subsequent views. As I said in the comments, this is either a result of poor implementation on Apple's part or just dreadful documentation (who knows, maybe there is a "correct" way to accomplish this).
Whatever the case, I came up with a workaround that seems to produce the original poster's desired results. I'm hesitant to recommend it because it seems unnecessarily hacky, but without any straightforward way of hiding and unhiding the navigation bar, this is the best I could do.
This example uses three views - View1 has a hidden navigation bar, and View2 and View3 both have visible navigation bars with titles.
struct View1: View {
#State var isNavigationBarHidden: Bool = true
var body: some View {
NavigationView {
ZStack {
Color.red
NavigationLink("View 2", destination: View2(isNavigationBarHidden: self.$isNavigationBarHidden))
}
.navigationBarTitle("Hidden Title")
.navigationBarHidden(self.isNavigationBarHidden)
.onAppear {
self.isNavigationBarHidden = true
}
}
}
}
struct View2: View {
#Binding var isNavigationBarHidden: Bool
var body: some View {
ZStack {
Color.green
NavigationLink("View 3", destination: View3())
}
.navigationBarTitle("Visible Title 1")
.onAppear {
self.isNavigationBarHidden = false
}
}
}
struct View3: View {
var body: some View {
Color.blue
.navigationBarTitle("Visible Title 2")
}
}
Setting navigationBarHidden to false on views deeper in the navigation stack doesn't seem to properly override the preference of the view that originally set navigationBarHidden to true, so the only workaround I could come up with was using a binding to change the preference of the original view when a new view is pushed onto the navigation stack.
Like I said, this is a hacky solution, but without an official solution from Apple, this is the best that I've been able to come up with.
View Modifiers made it easy:
//ViewModifiers.swift
struct HiddenNavigationBar: ViewModifier {
func body(content: Content) -> some View {
content
.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
}
}
extension View {
func hiddenNavigationBarStyle() -> some View {
modifier( HiddenNavigationBar() )
}
}
Example:
import SwiftUI
struct MyView: View {
var body: some View {
NavigationView {
VStack {
Spacer()
HStack {
Spacer()
Text("Hello World!")
Spacer()
}
Spacer()
}
.padding()
.background(Color.green)
//remove the default Navigation Bar space:
.hiddenNavigationBarStyle()
}
}
}
The purpose of a NavigationView is to add the navigation bar on top of your view. In iOS, there are 2 kinds of navigation bars: large and standard.
If you want no navigation bar:
FileBrowserView(jsonFromCall: URLRetrieve(URLtoFetch: applicationDelegate.apiURL))
If you want a large navigation bar (generally used for your top-level views):
NavigationView {
FileBrowserView(jsonFromCall: URLRetrieve(URLtoFetch: applicationDelegate.apiURL))
.navigationBarTitle(Text("Title"))
}
If you want a standard (inline) navigation bar (generally used for sub-level views):
NavigationView {
FileBrowserView(jsonFromCall: URLRetrieve(URLtoFetch: applicationDelegate.apiURL))
.navigationBarTitle(Text("Title"), displayMode: .inline)
}
Hope this answer will help you.
More information: Apple Documentation
iOS 14+
There is a dedicated modifier to make the navigation bar take less space:
.navigationBarTitleDisplayMode(.inline)
EDIT
In some cases it may still be needed to add .navigationBarHidden(true)
If you set the title as inline for the View you want remove the space on, this doesn't need to be done on a view with a NavigationView, but the one navigated too.
.navigationBarTitle("", displayMode: .inline)
then simply change the Navigation bars appearance
init() {
UINavigationBar.appearance().setBackgroundImage(UIImage(), for: .default)
UINavigationBar.appearance().shadowImage = UIImage()
}
on the view that holds the initial NavigationView.
If you want to change the Appearance from screen to screen change the appearance in the appropriate views
For me, I was applying the .navigationBarTitle to the NavigationView and not to List was the culprit. This works for me on Xcode 11.2.1:
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: DetailView()) {
Text("I'm a cell")
}
}.navigationBarTitle("Title", displayMode: .inline)
}
}
}
This is a bug present in SwiftUI (still as of Xcode 11.2.1). I wrote a ViewModifier to fix this, based on code from the existing answers:
public struct NavigationBarHider: ViewModifier {
#State var isHidden: Bool = false
public func body(content: Content) -> some View {
content
.navigationBarTitle("")
.navigationBarHidden(isHidden)
.onAppear { self.isHidden = true }
}
}
extension View {
public func hideNavigationBar() -> some View {
modifier(NavigationBarHider())
}
}
I also tried all the solutions mentioned on this page and only found #graycampbell solution the one to be working well, with well-working animations. So I tried to create a value I can just use throughout the app that I can access anywhere by the example of hackingwithswift.com
I created an ObservableObject class
class NavBarPreferences: ObservableObject {
#Published var navBarIsHidden = true
}
And pass it on to the initial view in the SceneDelegate like so
var navBarPreferences = NavBarPreferences()
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(navBarPreferences))
Then in the ContentView we can keep track of this Observable object like so and create a link to SomeView:
struct ContentView: View {
//This variable listens to the ObservableObject class
#EnvironmentObject var navBarPrefs: NavBarPreferences
var body: some View {
NavigationView {
NavigationLink (
destination: SomeView()) {
VStack{
Text("Hello first screen")
.multilineTextAlignment(.center)
.accentColor(.black)
}
}
.navigationBarTitle(Text(""),displayMode: .inline)
.navigationBarHidden(navBarPrefs.navBarIsHidden)
.onAppear{
self.navBarPrefs.navBarIsHidden = true
}
}
}
}
And then when accessing the second view (SomeView), we hide it again like this:
struct SomeView: View {
#EnvironmentObject var navBarPrefs: NavBarPreferences
var body: some View {
Text("Hello second screen")
.onAppear {
self.navBarPrefs.navBarIsHidden = false
}
}
}
To keep previews working add the NavBarPreferences to the preview like so:
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
SomeView().environmentObject(NavBarPreferences())
}
}
You don't need to set the title, you can simply use .stack
NavigationView {
VStack {
Color.cyan
}
.navigationBarHidden(true)
}
.navigationViewStyle(.stack) // Here
Put on your NextView the following code
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
But while pushing to the NextView via NavigationLink you have to put also the modifier like this :
NavigationLink(
destination: NextView()
.navigationBarTitle("")
.navigationBarHidden(true)
) {
Text("NEXT VIEW")
}
You could extend native View protocol like this:
extension View {
func hideNavigationBar() -> some View {
self
.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
}
}
Then just call e.g.:
ZStack {
*YOUR CONTENT*
}
.hideNavigationBar()
For me it was because I was pushing my NavigationView from an existing. In effect having one inside the other. If you are coming from a NavigationView you do not need to create one inside the next as you already inside a NavigatonView.
My solution for this problem was the same as suggested by #Genki and #Frankenstein.
I applied two modifiers to the inner list (NOT the NavigationView) to get rid of the spacing:
.navigationBarTitle("", displayMode: .automatic)
.navigationBarHidden(true)
On the outer NavigationView, then applied .navigationBarTitle("TITLE") to set the title.
I try to add .navigationBarHidden(true) at the end of curly brackets of my Vstack like this
NavigationView { Vstack(){"some Code"}.navigationBarHidden(true)}
and the navigation bar disappear but if i add .navigationBarHidden(true) at the end of curly brackets of navigation bar like this
NavigationView { Vstack(){"some Code"}}.navigationBarHidden(true)
the navigation bar doesn't disappear
Same problem, I finally solved. For the navigation to completely disappear, you need to add these modifier to the NavigationView AND ALL NavigationsLinks inside it:
.navigationBarHidden(true)
.navigationBarTitleDisplayMode(.inline)
If you don't do it also with the NavigationLinks won't work.
Similar to the answer by #graycampbell but a little simpler:
struct YourView: View {
#State private var isNavigationBarHidden = true
var body: some View {
NavigationView {
VStack {
Text("This is the master view")
NavigationLink("Details", destination: Text("These are the details"))
}
.navigationBarHidden(isNavigationBarHidden)
.navigationBarTitle("Master")
.onAppear {
self.isNavigationBarHidden = true
}
.onDisappear {
self.isNavigationBarHidden = false
}
}
}
}
Setting the title is necessary since it is shown next to the back button in the views you navigate to.
I tried setting up
.navigationBarTitle("", displayMode: .inline) .navigationBarHidden(true)
But it wasn't working. The issue was I was setting it to
NavigationView{...}.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
But to get rid of the NagigationBar it should be set to inner view of it
NavigationView{
InnerView{}.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
}
Hope this helps
To see in action, You can look into this open source App(WIP) https://github.com/deepaksingh4/KidsBookApp
I have had a similar problem when working on an app where a TabView should be displayed once the user is logged in.
As #graycampbell suggested in his comment, a TabView should not be embedded in a NavigationView, or else the "blank space" will appear, even when using .navigationBarHidden(true)
I used a ZStack to hide the NavigationView. Note that for this simple example, I use #State and #Binding to manage the UI visibility, but you may want to use something more complex such as an environment object.
struct ContentView: View {
#State var isHidden = false
var body: some View {
ZStack {
if isHidden {
DetailView(isHidden: self.$isHidden)
} else {
NavigationView {
Button("Log in"){
self.isHidden.toggle()
}
.navigationBarTitle("Login Page")
}
}
}
}
}
When we press the Log In button, the initial page disappears, and the DetailView is loaded. The Login Page reappears when we toggle the Log Out button
struct DetailView: View {
#Binding var isHidden: Bool
var body: some View {
TabView{
NavigationView {
Button("Log out"){
self.isHidden.toggle()
}
.navigationBarTitle("Home")
}
.tabItem {
Image(systemName: "star")
Text("One")
}
}
}
}
I struggled on this for a while, but what finally worked for me is...
ZStack {
...
}
.edgesIgnoringSafeArea(.all) //or .edgesIgnoringSafeArea(.top)
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
I have to navigate screen1 to screen2. If I use this for NavigationView like above answer Navigation bar will be hidden but its space still exist ( amount of space with height) in Screen 1.
Finally I have own solution that use this code in any view inside NavigationView and don't care about navigationBarTitle. Just like this:
Screen1:
NavigationView {
SomeView {
NavigationLink {
// go to screen2
}
}.navigationBarHidden(true)
}
Screen2:
NavigationView {
// some Views
}.navigationBarHidden(true)
I had the same issue and found the following code to work best.
.navigationTitle("")
.navigationBarBackButtonHidden(true)
This is by far the most simplest and stable approach I've found. You can hide both navigation title and back button by hiding the whole toolbar. You can show also choose to show it in any view you wish to. You can hide it by using .toolbar(.hidden) and make it visible by using the .toolbar(.visible) modifier.
iOS 16+
struct ContentView: View {
var body: some View {
NavigationStack {
List {
ForEach(0..<10) { i in
NavigationLink {
Text("Detail for Row \(i)")
} label: {
Text("Row \(i)")
}
}
}
.toolbar(.hidden)
}
}
}
If you targeting below iOS 16, you can replace the NavigationStack with NavigationView.
Try putting the attributes (navigation title, toolbar, etc) outside of the Navigation View. Like so:
NavigationView {
}
.navigationTitle("Detail News")
.toolbarColorScheme(.dark, for: .navigationBar)
.toolbarBackground(Color.gray, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.accentColor(.white)
Really loved the idea given by #Vatsal Manot To create a modifier for this.
Removing isHidden property from his answer, as I don't find it useful as modifier name itself suggests that hide navigation bar.
// Hide navigation bar.
public struct NavigationBarHider: ViewModifier {
public func body(content: Content) -> some View {
content
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
extension View {
public func hideNavigationBar() -> some View {
modifier(NavigationBarHider())
}
}
I know I'm a bit late here, but I just fixed this problem using the top answer here:
How to get rid of space in nested NavigationView with SwiftUI
In case the content of that page changes, I'll explain the answer below.
Only use a NavigationView wrapper at the very top level of any view that needs navigation, no matter how far the nested children go down. They will all already have NavigationView properties and you can call NavigationLink at any time within the sub-views. I had a lot of extra NavigationView wrappers around sub-views, deleting them removed the extra white space while retaining the functionality of all navigation links.
Try putting the NavigationView inside a GeometryReader.
GeometryReader {
NavigationView {
Text("Hello World!")
}
}
I’ve experienced weird behavior when the NavigationView was the root view.
Related
Screen shot of white space
I want to remove the empty space below the <Back button in the second navigation view. I know that this question has been asked several times before, but I have not been able to find a good solution that does this.
I have tried
.navigationBarTitle("")
.navigationBarHidden(true)
and
.navigationBarTitle("", displayMode: .inline)
without the desired result.
Any hints that could help me?
struct SecondNavView: View {
let item: String
var body: some View {
ZStack {
Color.red
Text(item)
}
}
}
struct FirstNavView: View {
let listItems = ["One", "Two", "Three"]
var body: some View {
NavigationView {
List(listItems, id: \.self) { item in
NavigationLink(destination: SecondNavView(item: item)) {
Text(item).font(.headline)
}
}
}
}
}
I assume it is do to place of applied modifiers.
The following works (tested with Xcode 13.4 / iOS 15.5)
struct SecondNavView: View {
let item: String
var body: some View {
ZStack {
Color.red
Text(item)
}
.navigationBarTitleDisplayMode(.inline) // << here !!
}
}
It seens like your parent View hasn't a title, to solve this you need to set .navigationTitle inside NavigationView on parent View like this:
NavigationView {
VStack {
//....
}
.navigationTitle(Text("Awesome View"))
.toolbar {
ToolbarItem(placement: .principal){
// Put any view (Text, Image, Stack...) you want here
}
}
}
I am developing an app with SwiftUI.
I have a NavigationView and I have buttons on the navigation bar. I want to replace the current view (which is a result of a TabView selection) with another one.
Basically, when the user clicks "Edit" button, I want to replace the view with another view to make the edition and when the user is done, the previous view is restored by clicking on a "Done" button.
I could just use a variable to dynamically choose which view is displayed on the current tab view, but I feel like this isn't the "right way to do" in SwiftUI. And this way I could not apply any transition visual effect.
Some code samples to explain what I am looking for.
private extension ContentView {
#ViewBuilder
var navigationBarLeadingItems: some View {
if tabSelection == 3 {
Button(action: {
print("Edit pressed")
// Here I want to replace the tabSelection 3 view by another view temporarly and update the navigation bar items
}) {
Text("Edit")
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView(selection: $tabSelection) {
ContactPage()
.tabItem {
Text("1")
}
.tag(1)
Text("Chats")
.tabItem() {
Text("2")
}
.tag(2)
SettingsView()
.tabItem {
Text("3")
}
.tag(3)
}.navigationBarItems(leading: navigationBarLeadingItems)
}
}
}
Thank you
EDIT
I have a working version where I simply update a toggle variable in my button action that makes my view display one or another thing, it is working but I cannot apply any animation effect on it, and it doesn't look "right" in SwiftUI, I guess there is something better that I do not know.
If you just want to add animations you can try:
struct ContentView: View {
...
#State var showEditView = false
var body: some View {
NavigationView {
TabView(selection: $tabSelection) {
...
view3
.tabItem {
Text("3")
}
.tag(3)
}
.navigationBarItems(leading: navigationBarLeadingItems)
}
}
}
private extension ContentView {
var view3: some View {
VStack {
if showEditView {
FormView()
.background(Color.red)
.transition(.slide)
} else {
Text("View 3")
.background(Color.blue)
.transition(.slide)
}
}
}
}
struct FormView: View {
var body: some View {
Form {
Text("test")
}
}
}
A possible alternative is to use a ViewRouter: How To Navigate Between Views In SwiftUI By Using An #EnvironmentObject.
I've got a problem with SwiftUI.
Basically this is my view graph:
View1 -> View2 -> View3 -> etc.
Now i want to show a back button in View2, but when user gets to view 3, it shouldn't be visible.
I tried hiding and removing items, but still i can see back button on next views which will get me back to View1.
As i was unable to do this the pure and nice way, i created a custom button, with dismiss action, which has content and the action works if only property isShowingButton is true, then on getting to next view i toggle it to false. It works, maybe not nicest way but works. If somehow someone knows a better way to solve it, i'd be grateful
Without seeing your Code it's quite hard to tell where the issue is.
However the following Code is working for me
struct ContentView: View {
var body: some View {
NavigationView {
FirstView()
}
}
}
struct FirstView: View {
var body: some View {
NavigationLink(destination: SecondView()
) {
Text("Go to Second View")
}
.navigationBarTitle("FirstView", displayMode: .inline)
}
}
struct SecondView: View {
var body: some View {
NavigationLink(destination: ThirdView().navigationBarBackButtonHidden(true)
) {
Text("Go to Third View")
}
.navigationBarTitle("SecondView", displayMode: .inline)
}
}
struct ThirdView: View {
var body: some View {
Text("Third View without Back Button")
.navigationBarTitle("Third View", displayMode: .inline)
}
}
When navigating to View 3 you need to add .navigationBarBackButtonHidden(true) to the NagivationLink
struct ViewTwo: View {
var body: some View {
NavigationLink(destination: SecondView().navigationBarBackButtonHidden(true)
) {
Text("Go to View 3")
}
.navigationBarTitle("View 2", displayMode: .inline)
}
}
I'm getting a strange crash from pretty normal navigation on my SwiftUI app
I have a simple tab view :
struct FFTabView: View {
var body: some View {
TabView {
LibraryView2()
}
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
.navigationBarTitle("", displayMode: .inline)
}
}
// MARK: -
struct LibraryView2: View {
var body: some View {
VStack {
NavigationLink(destination: Foo()) {
Text("go to foo")
}
}
.tabItem {
Image(systemName: "square.grid.2x2.fill")
Text("Skill Library")
}
}
}
struct Foo: View {
var body: some View {
Text("foo view")
}
}
When I go back via my nav bar, from Foo I get a crash: Tried to pop to a view controller that doesn't exist
Any idea what's going on here? I can't find anything related to this and SwiftUI so figured I'd post. Thanks
Although you didn't specify, I assume your FFTabView is wrapped in a NavigationView somewhere.
Ultimately, then, your view hierarchy looks like
NavigationView {
TabView {
NavigationLink {
...
}
}
}
If you restructure your view hierarchy so it's like
TabView {
NavigationView {
NavigationLink {
...
}
}
}
The crash doesn't happen.
Edit:
I have confirmed that it is related to the regression/bug discussed in this answer, introduced in Xcode 11.2. Your original code works fine in Xcode 11.1.
I have a NavigationView with a NavigationButton inside of it, but I cannot get the NavigationButton to be at the top of the screen and still be able to be pressed, even though the navigation bar is hidden.
This code:
struct ContentView : View {
var body: some View {
NavigationView {
VStack {
NavigationButton(destination: Text("Button Clicked")) {
Text("Hello World")
.background(Color.yellow)
}
Spacer()
}
}
.navigationBarHidden(true)
}
}
Looks like , but I want it to look like .
I've tried adding a negative padding to the top of the VStack (with .padding([.top], -95), and it visually works, but then I can't interact with the button by tapping it (I think it is behind the hidden navigation bar). I've tried setting the VStack's zIndex to 10000 to solve that, but it still didn't work. Is there a way for me to move the button up to the top while still making sure that the button recognizes when it is being tapped?
Add a navigationBarTitle before hiding your navigation bar:
struct ContentView : View {
var body: some View {
NavigationView {
VStack {
NavigationButton(destination: Text("Button Clicked")) {
Text("Hello World")
.background(Color.yellow)
}
Spacer()
}
.navigationBarTitle(Text("Title")) // Add this line
.navigationBarHidden(true)
}
}
Add this modifier to your NavigationView edgesIgnoringSafeArea(.top).