iosswiftuiswiftui-searchable

SwiftUI searchable becomes visible unexpectedly after toggling views in NavigationStack


I'm trying to implement in SwiftUI a toggle that displays either a list or a grid of bookmarks, depending on a layoutStyle state. I'm also using .searchable to allow users to filter bookmarks.

The problem I'm encountering is:

I’d like the search bar to remain hidden unless the user explicitly taps on it, even after toggling between views.

This is the toggling code I'm using in the ContentView:

    @State private var searchText: String = ""
    @State var layoutStyle = LayoutStyle.list
    
    private let bookmarks = Model.withTestData().bookmarks
    
    var body: some View {
        NavigationStack {
            Group {
                if layoutStyle == .list {
                    ListView(bookmarks: bookmarks)
                } else {
                    GridView(bookmarks: bookmarks)
                }
            }
            .searchable(text: $searchText)
            .navigationTitle("Bookmarks")
            .navigationBarTitleDisplayMode(.automatic)
            .toolbar {
                ToolbarItem(id: "", placement: .primaryAction) {
                    Button("Switch", systemImage: "square.grid.2x2") {
                        layoutStyle.toggle()
                    }
                }
            }
        }
    }

I've tried applying the searchable, navigationTitle and navigationBarTitleDisplayMode modifiers on ListView and GridView but it's behaving the same. Also I've tried applying id(layoutStyle) on the NavigationStack but this only fixed it for the grid.

Is this a SwiftUI bug or am I missing something? Any help on how to fix this would be greatly appreciated!


Solution

  • You said in the question:

    I’d like the search bar to remain hidden unless the user explicitly taps on it

    It may be difficult for the user to tap on something that is hidden. But the search bar can be made visible by pulling the list/grid down.

    To prevent the search bar from appearing when switching between layout styles, it helps to launch the child views with an empty array, then populate the array in .onAppear.

    Here is the fully elaborated example to show it working:

    struct ContentView: View {
        @State private var searchText: String = ""
        @State var layoutStyle = LayoutStyle.list
    
        private let bookmarks = (1...100).map { "Bookmark \($0)" } // Model.withTestData().bookmarks
        @State private var activeBookmarks = [String]()
    
        var body: some View {
            NavigationStack {
                Group {
                    if layoutStyle == .list {
                        ListView(bookmarks: activeBookmarks)
                            .onAppear { activeBookmarks = bookmarks }
                    } else {
                        GridView(bookmarks: activeBookmarks)
                            .onAppear { activeBookmarks = bookmarks }
                    }
                }
                .searchable(text: $searchText)
                .navigationTitle("Bookmarks")
                .navigationBarTitleDisplayMode(.automatic)
                .toolbar {
                    ToolbarItem(id: "", placement: .primaryAction) {
                        Button("Switch", systemImage: "square.grid.2x2") {
                            withAnimation {
                                activeBookmarks.removeAll()
                                layoutStyle.toggle()
                            }
                        }
                    }
                }
            }
        }
    }
    
    enum LayoutStyle {
        case list
        case grid
    
        mutating func toggle() {
            self = self == .list ? .grid : .list
        }
    }
    
    struct ListView: View {
        let bookmarks: [String]
    
        var body: some View {
            List {
                ForEach(Array(bookmarks.enumerated()), id: \.offset) { index, bookmark in
                    Text(bookmark)
                }
            }
        }
    }
    
    struct GridView: View {
        let bookmarks: [String]
    
        var body: some View {
            ScrollView {
                LazyVGrid(columns: [.init(.flexible()), .init(.flexible())]) {
                    ForEach(Array(bookmarks.enumerated()), id: \.offset) { index, bookmark in
                        Text(bookmark)
                            .padding()
                            .frame(maxWidth: .infinity)
                            .background {
                                RoundedRectangle(cornerRadius: 8)
                                    .fill(.background)
                                    .stroke(.gray)
                            }
                    }
                }
                .padding(.horizontal)
            }
            .background(Color(.systemGroupedBackground))
        }
    }
    

    Animation