Sheffield
Sheffield

Reputation: 413

List is jumpy when items are changed

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.

enter image description here

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

Answers (1)

Asperi
Asperi

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

Related Questions