iosswiftswiftuitoolbar

How to use a custom multiline toolbar in SwiftUI?


I have some code that works fine, but when I navigate back from DetailView, the toolbar appears cropped. Do you know how to fix it properly?

struct ContentView: View {
    var body: some View {
        NavigationView {
            List  {
                Section {
                    ForEach(0..<10) { index in
                        NavigationLink(destination: DetailView()) {
                            Text("Row \(index+1)")
                        }
                    }
                }
            }
            .scrollContentBackground(.hidden)
            .background(Color.blue)
            .toolbar {
                multilineToolbar
            }
        }
    }

    var multilineToolbar: some ToolbarContent {
        ToolbarItem(placement: .principal) {
            VStack {
                Text("Line 1")
                    .font(.largeTitle)
                Text("Line 2")
                    .font(.title2)
            }
        }
    }
}

Image 1Image 2


Solution

  • The toolbar is not intended to be used for content with significant height and it may not be possible to increase the height of the space available.

    As a workaround, you could show the header using .safeAreaInset(edge: .top) instead. To emulate the behavior of the default toolbar, a material background can be added when the List is scrolled.

    With iOS 18, scrolling can be detected using onScrollGeometryChange:

    struct ContentView: View {
        @State private var isScrolled = false
    
        var body: some View {
            NavigationStack {
                List  {
                    Section {
                        ForEach(0..<10) { index in
                            NavigationLink(destination: DetailView()) {
                                Text("Row \(index+1)")
                            }
                        }
                    }
                }
                .onScrollGeometryChange(for: Bool.self) { geometry in
                    geometry.contentOffset.y + geometry.contentInsets.top > 0
                } action: { _, newVal in
                    isScrolled = newVal
                }
                .scrollContentBackground(.hidden)
                .background(Color.blue)
                .contentMargins(.top, 0)
                .toolbarVisibility(.hidden, for: .navigationBar)
                .safeAreaInset(edge: .top) {
                    multilineHeading
                }
            }
        }
    
        private var multilineHeading: some View {
            VStack {
                Text("Line 1")
                    .font(.largeTitle)
                Text("Line 2")
                    .font(.title2)
            }
            .padding(.bottom, 6)
            .frame(maxWidth: .infinity)
            .background {
                if isScrolled {
                    ZStack(alignment: .bottom) {
                        Rectangle()
                            .fill(.bar)
                            .ignoresSafeArea()
                        Divider()
                    }
                }
            }
            .animation(.easeInOut(duration: 0.2), value: isScrolled)
        }
    }
    

    Animation


    You were using NavigationView in your example. This should only be necessary if you still need to support iOS 15. In any case, if you need to support a version older than iOS 18, scrolling can be detected using a GeometryReader behind the section header instead. The answer to Change NavigationStack title font, tint, and background in SwiftUI provides an example of this technique being used (it was my answer).