swiftswiftuinavigationbarpicker

How add a segmented picker under the title in a NavigationBar on SwiftUI?


I'm trying to add a segmented picker in the NavigationBar under the title with SwiftUI. I've tried with the toolbar, but it was show on the top and not under the title.

How can I achieve the result like the image below in SwiftUI?

Screenshot from the App Store leaderboard.

This is what I have tried but doesn't work.


struct ExampleView: View {

    @State private var selectedTab: String = "tab1"

    var body: some View {
        VStack {
            Picker("Mode", selection: $selectedTab) {
                Text("tab1").tag("tab1")
                Text("tab2").tag("tab2")
                Text("tab3").tag("tab3")
            }
            .pickerStyle(.segmented)
            .padding([.horizontal, .top])
        
            Form {
                switch selectedTab {
                    case "tab1":
                        TabOneView()
                    case "tab2":
                        TabTwoView()
                    case "tab3":
                        TabThreeView()
                    default:
                        TabOneView()
                }
            }
        }
        .navigationTitle("Title")
        .navigationBarTitleDisplayMode(.inline)
    }
}

Solution

  • I've just been watching a similar thing in one of Kavsoft's videos on YouTube on how to make a sticky header, it's a super interesting topic and there is no default way to do it.

    There's 2 important points in the video:

    1. where he hides the default toolbar background (the thin material with divider);
    2. and where he adds the view to the safe area and brings back the toolbar background.

    I will also list the stages here:

    1. Add the segmented picker inside the safe area top inset, which pins it to the top even when scrolling, with the .safeAreaInset(edge: .top) modifier.

    1. Give the segmented picker (safe area inset contents) a background that resembles the original toolbar background, for example:
    .padding(.bottom)
    .background {
        Rectangle()
            .fill(.ultraThinMaterial)
            .ignoresSafeArea()
    }
    

    Edit!!! Instead of trying to emulate the toolbar background, you can access it with .background(.bar). Thanks to @BenzyNeez for this suggestion.

    1. Delete the default toolbar background (with either .toolbarBackgroundVisibility(.hidden, for: .navigationBar) or .toolbarBackground(.hidden, for: .navigationBar).

    Code implementation:

    I implemented it in code for you:

    
    import SwiftUI
    
    enum Option {
        case a, b
        var text: String {
            switch self {
            case .a: "App gratuite"
            case .b: "App a pagamento"
            }
        }
    }
    
    struct SticktoHeader: View {
        
        @State private var tab = Option.a
        
        var body: some View {
            
            NavigationStack {
                
                // List stuff
                List(0..<50) { n in
                    Text("Item \(n)")
                }
                .navigationTitle("Classifiche")
                .toolbar {
                    // Add a default toolbar
                    ToolbarItem(placement: .topBarTrailing) {
                        Button("Tutte le app") { }
                    }
                }
                
                // NEW 1: Add safe area inset
                .safeAreaInset(edge: .top) {
                    Picker("Pick tab", selection: $tab) {
                        Text(Option.a.text)
                            .tag(Option.a)
                        
                        Text(Option.b.text)
                            .tag(Option.b)
                    }
                    .padding(.horizontal)
                    .padding(.bottom)
                    .pickerStyle(.segmented)
                    //.background {
                        //Rectangle()
                            //.fill(.ultraThinMaterial)
                            //.ignoresSafeArea()
                            //.overlay(alignment: .bottom) {
                                //Divider()
                            //}
                    //}
                    .background(.bar) // Thanks @BenzyKneez
                }
                .toolbarBackground(.hidden, for: .navigationBar)
                
            }
            
            
        }
    }
    
    #Preview {
        SticktoHeader()
    }