SwiftUI dismissSearch from .onSubmit(of: .search) modifier func does not seem possible

I'm wondering if I need to submit a bug report for this or if this is really what Apple intends for SwiftUI. Ideally I can find out I'm wrong.

I have a .searchable List/Feature that can trigger my search function from the .onSubmit(of: .search) modifier function. When I do this it seems that there is no way to perform the same behavior as dismissSearch the Environment Value in this function. I have several views that are interchangeable inside this List's container that are all listening for my ViewModels isSearchingActive published property. I set my ViewModel.isSearchingActive by listening to changes of the child view's Environment Value of isSearching. I also listen for changes to ViewModel.isSearchingActive in my children to call the Environment Value dismissSearch. This does not seem to work.

Here is a playground Minimal Reproducible Example. I am hoping I do not need to make a bug report, I hope I am just wrong... Thanks in advance!

import SwiftUI
import PlaygroundSupport

struct SearchingExample: View {
    @State var searchText = ""
    @State var didSubmit = false

    var body: some View {
        NavigationStack {
            SearchedView(didSubmit: $didSubmit)
                .searchable(text: $searchText)
                .onSubmit(of: .search) {
                    didSubmit = true
                }
        }
    }
}

struct SearchedView: View {
    @Environment(\.isSearching) var isSearching
    @Environment(\.dismissSearch) var dismissSearch
    @Binding var didSubmit: Bool

    var body: some View {
        VStack {
            Text(isSearching ? "Searching!" : "Not searching.")
            Button(action: { dismissSearch() }, label: {
                Text("Dismiss Search")
            })
            Button(action: {
                // Return Key for playground
            }, label: {
                Image(systemName: "paperplane")
            })
            .frame(width: 30, height: 30)
            .keyboardShortcut(.defaultAction)
            if didSubmit {
                Text("You Submitted Search!")
                    .onAppear {
                        Task { @MainActor in
                            try await Task.sleep(nanoseconds: 3_000_000_000)
                            self.didSubmit = false
                        }
                    }
            }
        }
    }
}

PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.setLiveView(SearchingExample())
Answered by mellis94 in 771172022

@Polyphonic Solved this for me but the example they posted did not work for me in the xcode playground I had to prove it.

In order to call dismissSearch from a parent view's .onSubmit(of: .search) one must find a way to store the reference to the child view's dismissSearch environment value in a way that the parent can call it. For example, use a ViewModel of some kind to store a function that you can overwrite from the child with the child's dismissSearch environment value when it becomes available in an onAppear or something similar.

class ViewModel {
    var dismissClosure: () -> Void = { print("Not Set") }
}

Pass this ViewModel to the child and then store the dismissSearch in the child so the parent can call it from the shared ViewModel

.onAppear {
    viewModel.dismissClosure = { dismissSearch() }
}

Here is the complete solution to the posted problem.

import SwiftUI
import PlaygroundSupport

class ViewModel {
    var dismissClosure: () -> Void = { print("Not Set") }
}
struct SearchingExample: View {
    @State var searchText = ""
    @State var didSubmit = false
    @Environment(\.dismissSearch) var dismissSearch
    let viewModel = ViewModel()
    
    var body: some View {

        NavigationStack {
            SearchedView(didSubmit: $didSubmit, viewModel: viewModel)
                .searchable(text: $searchText)
                .onSubmit(of: .search) {
                    didSubmit = true
                    viewModel.dismissClosure()
                }
        }
    }
}

struct SearchedView: View {
    @Environment(\.isSearching) var isSearching
    @Environment(\.dismissSearch) var dismissSearch
    @Binding var didSubmit: Bool
    let viewModel: ViewModel

    var body: some View {
        VStack {
            Text(isSearching ? "Searching!" : "Not searching.")
            Button(action: { dismissSearch() }, label: {
                Text("Dismiss Search")
            })
            Button(action: {
                // Return Key for playground
            }, label: {
                Image(systemName: "paperplane")
            })
            .frame(width: 30, height: 30)
            .keyboardShortcut(.defaultAction)
            if didSubmit {
                Text("You Submitted Search!")
                    .onAppear {
                        Task { @MainActor in
                            try await Task.sleep(nanoseconds: 3_000_000_000)
                            self.didSubmit = false
                        }
                    }
            }
        }
        .onAppear {
            viewModel.dismissClosure = { dismissSearch() }
        }
    }
}

PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.setLiveView(SearchingExample())

It seems to me that your issue is not about the dismissSearch function itself, but rather that you can't get to the environment property inside the onSubmit closure.

One approach that's worked for me is to have your View body create a closure that calls into the environment property, and capture that closure in the onSubmit closure. Your View could look something like this:

struct SearchingExample: View {
    @State var searchText = ""
    @State var didSubmit = false
    @Environment(\.dismissSearch) var dismissSearch

    var body: some View {
        let dismissClosure: () -> Void = { dismissSearch() }
        NavigationStack {
            SearchedView(didSubmit: $didSubmit)
                .searchable(text: $searchText)
                .onSubmit(of: .search) {
                    didSubmit = true
                    dismissClosure()
                }
        }
    }
}

The main thing to be careful of here is that the dismissSearch function only works when a dismissible search is the current "context", but the code structure pretty much ensures that.

Note: The underlying issue here is that the environment property dismissSearch is not actually a function (I assume for technical reasons internal to SwiftUI), but a struct configured with the "call-as-function" feature, which allows it to behave as a call to a pre-determined function when you write () after a value of the type. Wrapping it in a closure is a simple way of "converting" it to a regular function when the environment properly can't be accessed in a local scope.

Accepted Answer

@Polyphonic Solved this for me but the example they posted did not work for me in the xcode playground I had to prove it.

In order to call dismissSearch from a parent view's .onSubmit(of: .search) one must find a way to store the reference to the child view's dismissSearch environment value in a way that the parent can call it. For example, use a ViewModel of some kind to store a function that you can overwrite from the child with the child's dismissSearch environment value when it becomes available in an onAppear or something similar.

class ViewModel {
    var dismissClosure: () -> Void = { print("Not Set") }
}

Pass this ViewModel to the child and then store the dismissSearch in the child so the parent can call it from the shared ViewModel

.onAppear {
    viewModel.dismissClosure = { dismissSearch() }
}

Here is the complete solution to the posted problem.

import SwiftUI
import PlaygroundSupport

class ViewModel {
    var dismissClosure: () -> Void = { print("Not Set") }
}
struct SearchingExample: View {
    @State var searchText = ""
    @State var didSubmit = false
    @Environment(\.dismissSearch) var dismissSearch
    let viewModel = ViewModel()
    
    var body: some View {

        NavigationStack {
            SearchedView(didSubmit: $didSubmit, viewModel: viewModel)
                .searchable(text: $searchText)
                .onSubmit(of: .search) {
                    didSubmit = true
                    viewModel.dismissClosure()
                }
        }
    }
}

struct SearchedView: View {
    @Environment(\.isSearching) var isSearching
    @Environment(\.dismissSearch) var dismissSearch
    @Binding var didSubmit: Bool
    let viewModel: ViewModel

    var body: some View {
        VStack {
            Text(isSearching ? "Searching!" : "Not searching.")
            Button(action: { dismissSearch() }, label: {
                Text("Dismiss Search")
            })
            Button(action: {
                // Return Key for playground
            }, label: {
                Image(systemName: "paperplane")
            })
            .frame(width: 30, height: 30)
            .keyboardShortcut(.defaultAction)
            if didSubmit {
                Text("You Submitted Search!")
                    .onAppear {
                        Task { @MainActor in
                            try await Task.sleep(nanoseconds: 3_000_000_000)
                            self.didSubmit = false
                        }
                    }
            }
        }
        .onAppear {
            viewModel.dismissClosure = { dismissSearch() }
        }
    }
}

PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.setLiveView(SearchingExample())
SwiftUI dismissSearch from .onSubmit(of: .search) modifier func does not seem possible
 
 
Q