2

In this SwiftUI-based code, the dismissSearch, as specified here, does not work. That is, upon tapping "Search" on the keyboard, I expected the search bar to cancel, clear the search text field, and have the nav bar with the gear icon visible. As you can see in the GIF below, the keyboard disappears (expected) but the search bar neither clears the text nor "cancels".

Do note that a print out of disappearSearch prints DismissSearchAction(state: nil).

What could be going on?

struct GroceryListView2: View {
    @Environment(\.dismissSearch) private var dismissSearch
    @State private var searchQuery = ""

    var body: some View {
        NavigationView {
            Text("Hi")
                .navigationBarItems(
                    trailing:
                        Button(action: {}, label: { Image(systemName: "gear")})
                )
                .searchable(
                    text: $searchQuery,
                    placement: .navigationBarDrawer(displayMode: .automatic),
                    prompt: "Add or search"
                )
                .onSubmit(of: .search) {
                    print("onSubmit", dismissSearch) // DismissSearchAction(state: nil)
                    dismissSearch()
                }
        }

    }
}

GIF

fingia
  • 504
  • 7
  • 20

1 Answers1

1

Wow. You have stumbled on some really weird behavior:

  • The @Environment(\.dismissSearch) property doesn't get populated with a useful value if it's in the same View that applies the .searchable modifier. My guess is searchable is responsible for putting the DismissSearchAction in the environment.

  • The onSubmit modifier doesn't work if it's applied inside (before) the searchable modifier. My guess is onSubmit stores the callback in the environment for searchable to read.

This leads to needing a convoluted view structure where you apply onSubmit outside (after) the searchable modifier and have it set an @State property which you pass down as a Binding and read in an onChange modifier on your inner view, where you have the @Environment(\.dismissSearch) property available.

I've wrapped it all up into a handy modifier, searchableOnce, which is like searchable but dismisses on submit. Here's how your code looks with this modifier:

struct GroceryListView2: View {
    @State private var searchQuery = ""

    var body: some View {
        NavigationView {
            Text("Hi")
                .navigationBarItems(
                    trailing:
                        Button(action: {}, label: { Image(systemName: "gear")})
                )
                .searchableOnce(
                    text: $searchQuery,
                    placement: .navigationBarDrawer(displayMode: .automatic),
                    prompt: "Add or search"
                )
        }
    }
}

And here's the modifier implementation:

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension SwiftUI.View {
  public func searchableOnce(text: SwiftUI.Binding<Swift.String>, placement: SwiftUI.SearchFieldPlacement = .automatic, prompt: SwiftUI.Text? = nil) -> some SwiftUI.View {
      return SearchableOnce(content: self, text: text, placement: placement, prompt: prompt)
  }

  public func searchableOnce(text: SwiftUI.Binding<Swift.String>, placement: SwiftUI.SearchFieldPlacement = .automatic, prompt: SwiftUI.LocalizedStringKey) -> some SwiftUI.View {
      return SearchableOnce(content: self, text: text, placement: placement, prompt: Text(prompt))
  }

  @_disfavoredOverload public func searchableOnce<S>(text: SwiftUI.Binding<Swift.String>, placement: SwiftUI.SearchFieldPlacement = .automatic, prompt: S) -> some SwiftUI.View where S : Swift.StringProtocol {
      return SearchableOnce(content: self, text: text, placement: placement, prompt: Text(prompt))
  }
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
fileprivate struct SearchableOnce<Content: View>: View {
    @State private var wantDismiss = false
    var content: Content
    var text: Binding<String>
    var placement: SearchFieldPlacement
    var prompt: Optional<Text>

    var body: some View {
        Dismisser(wantDismiss: $wantDismiss, content: content)
            .searchable(text: text, placement: placement, prompt: prompt)
            .onSubmit(of: .search) {
                wantDismiss = true
            }
    }
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
fileprivate struct Dismisser<Content: View>: View {
    @Environment(\.dismissSearch) private var dismissSearch
    @Binding var wantDismiss: Bool
    var content: Content

    var body: some View {
        content
            .onChange(of: wantDismiss) { newValue in
                if newValue {
                    dismissSearch()
                    wantDismiss = false
                }
            }
    }
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • interesting... would you be able to make searchableOnce accept a `suggestions` a là `.searchable`? – fingia Feb 13 '23 at 01:46
  • If you mean the [`searchable` modifier that has a `suggestedTokens` parameter](https://developer.apple.com/documentation/swiftui/view/searchable(text:tokens:suggestedtokens:placement:prompt:token:)-9m40k), I think you should be able to modify my code to wrap that version without too much difficulty. – rob mayoff Feb 13 '23 at 02:43