As said in the title, 2 of the 3 @AppStorage
variables aren't updated the first time. So, when the view is dismissed, I have to re-open it and then at the second dismiss they're all updated. To be precise, only storedCityName
is updated the first time.
Here's the view:
import SwiftUI
struct SearchView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var searchManager: SearchManager
@AppStorage("storedUserLatitude") private var storedUserLatitude: Double?
@AppStorage("storedUserLongitude") private var storedUserLongitude: Double?
@AppStorage("storedCityName") private var storedCityName: String?
var body: some View {
VStack {
TextField("", text: $searchManager.search)
ForEach(searchManager.searchResults, id: \.self) { result in
HStack {
VStack {
Text(result.title)
Text(result.subtitle)
}
}
.onTapGesture { update(result.title) }
}
}
}
private func update(_ city: String) {
searchManager.searchLocation(city)
storedUserLatitude = searchManager.userLocation?.coordinate.latitude
storedUserLongitude = searchManager.userLocation?.coordinate.longitude
storedCityName = city
dismiss()
}
}
And, here's the manager:
import Foundation
import MapKit
import Combine
class SearchManager: NSObject, ObservableObject, MKLocalSearchCompleterDelegate {
private let completer = MKLocalSearchCompleter()
@Published var search = ""
@Published var searchResults = [MKLocalSearchCompletion]()
@Published var userLocation: CLLocation?
private var publisher: AnyCancellable?
override init() {
super.init()
completer.delegate = self
completer.resultTypes = .address
publisher = $search
.receive(on: RunLoop.main)
.sink(receiveValue: { string in
self.completer.queryFragment = string
})
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
searchResults = completer.results
}
func searchLocation(_ city: String) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(city) { placemarks, error in
guard let placemark = placemarks?.first else { return }
self.userLocation = placemark.location
}
}
}
I really don't understand why it works at the second try but not at the first one.
I also tried to delay the dismiss
with DispatchQueue.main.asyncAfter(deadline: .now() + 2) { dismiss() }
to see if the @AppStorage
variables needed time to update but it doesn't change anything.
Do you have any idea of what's going on?
Thanks, in advance!
The real problem here is that you call your reverse geocoder with searchManager.searchLocation(city
), which uses an async function to do the reverse geocoding. While you pass a block to that to set the values in your search manager, the code in SearchView
isn't waiting for that information.
On the second try, your SearchManager environment object has had time to update with the geocoding from the first attempt.
You should consider updating searchLocation
to make sure that it only runs the subsequent code in update
once the geocoding has happened. One way would be to allow it to receive a block:
private func update(_ city: String) {
searchManager.searchLocation(city) {
storedUserLatitude = searchManager.userLocation?.coordinate.latitude
storedUserLongitude = searchManager.userLocation?.coordinate.longitude
storedCityName = city
dismiss()
}
}
// in SearchManager
func searchLocation(_ city: String, onComplete: () -> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(city) { placemarks, error in
guard let placemark = placemarks?.first else { return }
self.userLocation = placemark.location
onComplete()
}
}
There are neater ways, and you'd need to handle the error path properly, but hopefully this gives you the right idea.