I have a view model that holds the searchText
variable that is used in a view as a Binding for a textField
.
@Observable
final class SearchableViewModel {
var searchText: String = ""
func listenToSearchQuery() async {
for await searchQuery in searchText.async {
print(searchQuery)
}
}
}
struct SearchableView: View {
@State private var vm = SearchableViewModel()
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 20) {
Text("text")
Text("text")
}
.padding()
}
.searchable(text: $vm.searchText, prompt: Text("Search text..."))
.navigationTitle("Title")
.task {
await vm.listenToSearchQuery()
}
}
}
}
I know that prior iOS 17 we had @Published
and I could take the values from that String and use swift concurrency to listen to changes. Like this:
final class SearchableViewModel: ObservableObject {
@Published var searchText: String = ""
func listenToSearchQuery() async {
for await searchQuery in $searchText.values {
print(searchQuery)
}
}
}
struct SearchableView: View {
@StateObject private var vm = SearchableViewModel()
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 20) {
Text("text")
Text("text")
}
.padding()
}
.searchable(text: $vm.searchText, prompt: Text("Search text..."))
.navigationTitle("Title")
.task {
await vm.listenToSearchQuery()
}
}
}
}
Even with this solution when I change one letter in the search query I get 3 printed lines and I do not know why but at least it works observing changes compared to Observation framework.
How can I do that in iOS 17?
A few observations:
The
async
method “converts a non-asynchronous sequence into an asynchronous one.” AString
is a sequence ofCharacter
elements. Thus, if aString
contained the text “test”, theasync
function would return an asynchronous sequence of the characters “t”, “e”, “s”, and “t”. It is not for observing changes in aString
over time, but rather for asynchronously iterating through itsElement
values (i.e.,Character
), which is not what we wanted here.Instead, to observing changes of values over time with Observation framework, there are two basic approaches that leap to mind:
We might use
withObservationTracking
from within the view model. See SE-0395 – Observation for discussion/example rewithObservationTracking
.Warning: There are some weird idiosyncracies with this API. This does not really “track” anything. It only observes the first change. You have to call it recursively to observe all changes.
Also, it has a very non-conventional way of identifying what property is to be observed, so please refer to SE-0395’s discussion carefully. E.g., you must simply refer to the property in the first closure (e.g., they literally suggest you just
print
the property). I hope they adopt a more idiomatic pattern in the future rather than relying on some side-effect of aprint
statement. Lol.Or, we could use the traditional
onChange
view modifier:Regarding the duplicate searches, I would advise deduplicating it programmatically, e.g.:
Even when dealing with asynchronous sequences or Combine, we would do some sanitization like this, to avoid redundant searches. We would also frequently debounce to avoid too many soon-to-be-redundant searches, and you might want to do something like that here, too.