I have a search field and a List of search results. As the user types in the search field, the List is updated with new results. Crucially, with each update, I want to reset the List's scroll position back to the top. To achieve this, I'm using the following .onChange() modifier along with a ScrollViewReader:
.onChange(of: searchQuery) { _, newQuery in
Task {
searchResults = await searchLibrary(for: newQuery)
scrollViewProxy.scrollTo(0, anchor: .top)
}
}
My List uses index-based IDs, so scrolling to 0 should always go to the first item.
The above code works, but crashes if searchResults is empty because there is no item in the List with an ID of 0. (As a side note, it seems rather excessive for the scrollTo() method to trigger a full-on crash just because the ID is not found; I don't think this should be anything more than a warning, or the method should throw an error that can be caught).
To work around this, I added an isEmpty check, so we only attempt the scroll if the array is not empty:
.onChange(of: searchQuery) { _, newQuery in
Task {
searchResults = await searchLibrary(for: newQuery)
if !searchResults.isEmpty {
scrollViewProxy.scrollTo(0, anchor: .top)
}
}
}
However, even with this check, I was seeing rare crashes in production, consistent with a race condition. My guess is that when searchResults is updated, the view is not recreated immediately, so if scrollTo() is called too quickly, the List may not yet be seeing the latest update to the searchResults array.
I figured that I could try to delay the calling of scrollTo() to give the view time to update:
.onChange(of: searchQuery) { _, newQuery in
Task {
searchResults = await searchLibrary(for: newQuery)
if !searchResults.isEmpty {
DispatchQueue.main.async {
scrollViewProxy.scrollTo(0, anchor: .top)
}
}
}
}
However, even with this, I've just received a crash report pointing to the same issue (the first in about four months). I'm not able to reproduce the bug myself – so it definitely seems like a rare race condition, probably relating to the timing of view updates.
I guess, I can insert another isEmpty check before calling scrollTo(), but I'm starting to wonder if it's even possible to guarantee that the item will be in the List when scrollTo() performs its action, and because this is so hard to reproduce, I can't really test any ideas. Does anyone have any idea how (and at what point) the ScrollViewReader reads the view's current state? What's the right way to approach debugging a problem like this?
Moreover, does anyone have any better suggestions about how to handle resetting the List position? The reason I want to do this is because, if the user types, scrolls a bit, and then types some more, the new results appear above the fold where the user can't see them, leading to a confusing experience.
I thought about switching to the newer .scrollPosition() modifier, but that's only iOS 18+ and only for ScrollViews, not Lists.
Cheers!