124
votes

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

Current Version

What I want to accomplish is this

Ideal Version

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 Cells 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.

19

19 Answers

190
votes

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.

24
votes

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.

enter image description here

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

22
votes

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: enter image description here

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()
        }
    }
}
14
votes

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)

Starting issue solution 1 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. final solution

If you want to change the Appearance from screen to screen change the appearance in the appropriate views

10
votes

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())
    }
}
8
votes

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)
        }
    }
}

Navigation bar and list with no gap at the top

7
votes

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())
    }
}
7
votes

SwiftUI 2

There is a dedicated modifier to make the navigation bar take less space:

.navigationBarTitleDisplayMode(.inline)

There's no longer need to hide the navigation bar or set its title.

6
votes

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()
6
votes

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.

3
votes

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.

3
votes

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.

3
votes

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")
        }
                    
0
votes

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")
            }
        }
    }
}
0
votes

I struggled on this for a while, but what finally worked for me is...

ZStack {
    ...
}
.edgesIgnoringSafeArea(.all) //or .edgesIgnoringSafeArea(.top)
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
0
votes

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

-1
votes

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())
    }
}
-1
votes

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 enter image description here 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 enter image description here

-8
votes

Try putting the NavigationView inside a GeometryReader.

GeometryReader {
    NavigationView {
        Text("Hello World!")
    }
}

I’ve experienced weird behavior when the NavigationView was the root view.