iosswiftswiftui

SwiftUI - Too much space between search bar and navigation title


I am working on a complex app with too much of code, but I tried creating a sample app with necessary running code to explain my problem.

So let's say I have a content view with a button which opens a modal/sheet (named "Main View") with a navigation title, a search bar and a button. In this main view, search bar and navigation title are positioned correctly. When I tap on button with main view, it navigates to another view (named "Sub View") which has search bar, navigation title and eventually will have a back button to navigate back to Main View. In this Sub View, I am seeing navigation title and search bar not positioned correctly i.e. there is a lot of space between navigation title and search bar.

Am not able to figure out the cause of it and how to fix it.. any ideas or thoughts on it? Is it because of 2 navigation stacks i.e. one in the SampleAppApp struct and another in MainView struct?

Code:

import SwiftUI
import Foundation
import Combine

@main
struct SampleAppApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationStack {
                ContentView()
            }
        }
    }
}

struct ContentView: View {
    @State private var showModal: Bool = false
    var body: some View {
        VStack {
            Button("Click ME", action: { self.showModal.toggle() })
        }
        .sheet(isPresented: $showModal) {
            ZStack {
                Color(red: 0.95, green: 0.95, blue: 0.97)
                MainView(isPresented: $showModal).padding(.top, 15.0)
            }
        }
    }
}

// MARK: Code for Main View after "Click ME" is clicked
struct MainView: View {
    @State private var searchText = ""
    let searchTextPublisher = PassthroughSubject<String, Never>()

    /// Set to true when the panel is displayed. Dismisses the panel when set to false.
    var isPresented: Binding<Bool>
    @State private var showSubView: Bool = false

    var body: some View {
        NavigationStack {
            ZStack {
                Color(red: 0.95, green: 0.95, blue: 0.97).edgesIgnoringSafeArea(.all)
                VStack {
                    VStack {
                        Button("Tap to open sub-view", action: {
                            self.showSubView.toggle()
                        }).padding(.top, 20.0)
                    }
                    .navigationDestination(isPresented: $showSubView) {
                        SubView()
                    }
                    
                    MainViewSearchResultListAndNavTitle()
                    .searchable(
                        text: $searchText,
                        placement: .navigationBarDrawer(displayMode: .always),
                        prompt: "search"
                    )
                    .onChange(of: searchText) { newText in
                        searchTextPublisher.send(newText) /// Publishes when a search term is changed. This is used to debounce the search.
                    }
                    Spacer()
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color(red: 0.95, green: 0.95, blue: 0.97))
            }
        }
        .presentationDetents([.large])
        .interactiveDismissDisabled(true)
        .presentationBackgroundInteraction(.enabled)
    }
}

/// Shows search bar and search results list.
struct MainViewSearchResultListAndNavTitle: View {
    var body: some View {
        VStack {
            // Contains search results list
        }
        .toolbar(content: {
            ToolbarItem(placement: .navigationBarLeading) {
                Text("Main View")
                    .bold()
                    .font(.largeTitle)
                    .padding(.bottom, 15.0)
            }
        })
        .toolbarBackground(Color(red: 0.95, green: 0.95, blue: 0.97), for: .navigationBar)
        .toolbarBackground(.visible, for: .navigationBar)
    }
}

// MARK: Code for Sub View after "Tap to open sub-view" is clicked. This view will have back button to go back to 2nd view i.e. "Main View".
struct SubView: View {
    var body: some View {
        ZStack {
            Color(red: 0.95, green: 0.95, blue: 0.97).edgesIgnoringSafeArea(.all)
            VStack {
                SubViewSearchResultsListAndNavTitle()
                .searchable(
                    text: .constant(""),
                    placement: .navigationBarDrawer(displayMode: .always),
                    prompt: "search"
                )
                Spacer()
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color(red: 0.95, green: 0.95, blue: 0.97))
        }
    }
}

private struct SubViewSearchResultsListAndNavTitle: View {
    var body: some View {
        VStack {
            // Contains search results list for sub view.
        }
        .toolbar(content: {
            ToolbarItem(placement: .principal) {
                Text("Sub view").bold()
                    .font(.title)
                    .padding(.bottom, 15.0)
            }
        })
        .toolbarBackground(Color(red: 0.95, green: 0.95, blue: 0.97), for: .navigationBar)
        .toolbarBackground(.visible, for: .navigationBar)
        .navigationBarBackButtonHidden(true)
    }
}

Screenshot:

enter image description here


Solution

  • Using Xcode's UI debugger, I found that the empty space is actually a _UINavigationBarLargeTitle, so that space is actually used to display the large navigation title, except you didn't set a navigationTitle, so it displays nothing.

    Using an .inline title style on the SubView removes the space.

    .navigationDestination(isPresented: $showSubView) {
        SubView().navigationBarTitleDisplayMode(.inline)
    }
    

    I'm not sure why you didn't need to do the same for MainView though. Perhaps it's some hidden logic in the .automatic style.