SwiftUI .searchable's new "scope" feature behavior

Hey everyone, I've been experimenting around with the new searchable update to add scope to SwiftUI search bars. I requested this last fall and now I'm looking into it as available with Xcode 14 / iOS 16 (FB9674003).

I found two bugs in case others run into them.

  1. The search bar's segmented 'scopes' only show if the search binding is a non-empty string. (FB10558607)
  2. The selected scope binding is not honored when updated outside of the searchable search bar itself. (FB10558881)

It is my assumption that these are both defects. Attached is a sample view that illustrates the two bugs.

Additionally, writing this up, I felt it was important to also provide developers the ability to specify the visibility of the segmented scopes within the search bar which is possible in UIKit (FB10558936). Something like .searchableScopeVisibility(.always).


import SwiftUI

struct ContentView: View {
    
    enum FoodScope: CaseIterable {
        case fruit
        case veggies
        
        func scopeText() -> String {
            switch self {
            case .fruit:
                return "Fruit"
                
            case .veggies:
                return "Veggies"
            }
        }
    }
    
    private let fruits: [String] = ["Apple", "Apricot", "Banana", "Cantaloupe"]
    private let veggies: [String] = ["Asparagus", "Beets", "Broccoli", "Cabbage"]
    
    @State private var searchText: String = ""
    @State private var scope: FoodScope = .fruit
    
    private var filteredFood: [String] {
        switch scope {
        case .fruit:
            guard searchText != "" else { return fruits }
            return fruits.filter { $0.contains(searchText) }
            
        case .veggies:
            guard searchText != "" else { return veggies }
            return veggies.filter { $0.contains(searchText) }
        }
    }
    
    var body: some View {
        NavigationStack {
            List {
                
                Section {
                    ForEach(filteredFood, id: \.self) { food in
                        NavigationLink(food, value: food)
                    }
                } header: {
                    Text("Food")
                        .textCase(.none)
                }
                
                // FB10558607 - SwiftUI: Searchable "scope" non functional in Xcode 14 beta 2 (scope items not visible when searching)
                
                Section {
                    
                } footer: {
                    Text("The 'scopes' provided within the new searchable modifier will only be shown when the searchable text binding is a non-empty string. Try for your self by tapping inside the search. You \"should\" see the scope segments appear right away but they don't. Then type any character and they'll show on screen. FB10558607")
                }

                // FB10558881 - SwiftUI: Searchable 'scope' binding is not honored when updated by another mechamsim outside of the searchable scope picker

                // Create another binding to the selected scope and change it. The picker in the search bar does NOT reflect the state of SwiftUI's @State scope property.
                Section {
                    Picker("Scope", selection: $scope) {
                        ForEach(FoodScope.allCases, id: \.self) { scope in
                            Text(scope.scopeText())
                                .tag(scope)
                        }
                    }
                    .pickerStyle(.segmented)
                    .buttonStyle(.plain)
                    .listRowBackground(Color.clear)
                } header: {
                    Text("Searchable Scope Binding Selector")
                        .textCase(.none)
                } footer: {
                    Text("Additionally, the scope binding will not update when modified via another mechanism (like another segmented picker). When the segments are visible with the search, change the scope and you'll see the binding to the picker change. But, if you change the scope of the picker below, the scope in the search bar will not react as expected. ")
                }
            }
            .listStyle(.insetGrouped)
            .searchable(text: $searchText, scope: $scope) {
                ForEach(FoodScope.allCases, id: \.self) { scope in
                    Text(scope.scopeText())
                        .tag(scope)
                }
            }
            .navigationTitle("FB9674003")
            .onChange(of: scope) { newValue in
                print("New scope \(newValue.scopeText())")
            }
            .navigationDestination(for: String.self) { value in
                Text("You selected \(value)")
                    .navigationTitle("Yummy food")
            }
        }
    }
}

#if DEBUG

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

#endif

Update: Xcode 14 beta 3 deprecated the above code using scope within searchable and now there is a new modifier .searchScopes. The behaviors described in my feedbacks still apply. I'd expect the scopes to be visible before typing in the search and the selected scope to respect the binding. These are still open issues. A visibility param now makes a ton of sense.

The new API is here: https://developer.apple.com/documentation/swiftui/anyview/searchscopes(_:scopes:)?changes=latest_minor

SwiftUI .searchable's new "scope" feature behavior
 
 
Q