Reputation: 413
I implemented a location search view with locations suggested per user query. The issue is every time when the input text is changed, there will be a momentary rendering error.
Here is the reproducible code snippet. I tried to remove the debounce implementation, but the issue still existed.
import SwiftUI
import MapKit
struct ContentView: View {
@StateObject private var vm = LocationChooserViewModel()
@State var query: String = ""
let center = CLLocationCoordinate2D(latitude: 116.3975, longitude: 39.9087)
var body: some View {
VStack {
TextField("Type to search", text: $query)
.onChange(of: query) { newQuery in
vm.scheduledSearch(by: newQuery, around: center)
}
List {
if vm.mapItems.count > 0 {
Section("Locations") {
ForEach(vm.mapItems, id: \.self) { mapItem in
Text(mapItem.name ?? "")
}
}
}
}
.listStyle(.grouped)
Spacer()
}
.padding()
}
}
class LocationChooserViewModel: ObservableObject {
@Published var mapItems: [MKMapItem] = []
private var timer: Timer? = nil
func scheduledSearch(by query: String, around center: CLLocationCoordinate2D) -> Void {
timer?.invalidate()
guard query.trimmingCharacters(in: .whitespaces) != "" else {
mapItems = []
return
}
timer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false, block: { _ in
self.performSearch(by: query, around: center)
})
}
private func performSearch(by query: String, around center: CLLocationCoordinate2D) -> Void {
let searchRequest = MKLocalSearch.Request()
searchRequest.naturalLanguageQuery = query
let search = MKLocalSearch(request: searchRequest)
search.start { response, _ in
self.mapItems = response?.mapItems ?? []
}
}
}
------------ Update 5/3 ------------
I happened to learn about the differences between ForEach
and List
in What is the difference between List and ForEach in SwiftUI?. I tried to replace ForEach(vm.mapItems)
by ForEach(0..<vm.mapItems.count)
. It works, but I'm still not sure whether it's an efficient way to do it.
Upvotes: 0
Views: 152
Reputation: 258423
It might be an effect of overlapped results delivery. Try to do two things: a) cancel previous search request, b) update result on main queue:
class LocationChooserViewModel: ObservableObject {
@Published var mapItems: [MKMapItem] = []
private var timer: Timer? = nil
private var search: MKLocalSearch? = nil // << here !!
func scheduledSearch(by query: String, around center: CLLocationCoordinate2D) -> Void {
timer?.invalidate()
guard query.trimmingCharacters(in: .whitespaces) != "" else {
mapItems = []
return
}
timer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false, block: { _ in
self.performSearch(by: query, around: center)
})
}
private func performSearch(by query: String, around center: CLLocationCoordinate2D) -> Void {
let searchRequest = MKLocalSearch.Request()
searchRequest.naturalLanguageQuery = query
self.search?.cancel() // << here !!
self.search = MKLocalSearch(request: searchRequest)
self.search?.start { response, _ in
DispatchQueue.main.async { // << here !!
self.mapItems = response?.mapItems ?? []
}
}
}
}
Alternate is to switch to Combine with .debounce
.
Upvotes: 1