I'm struggling with this for a long time without finding where I'm wrong (I know I'm wrong).
I have one API call with the location of the phone (this one is working), but I want the same API call with a manual location entered by a textfield (using Geocoding for retrieving Lat/Long). The geocoding part is ok and updated but not passed in the API call.
I also want this API call to be triggered when the TextField is cleared by the dedicated button back with the phone location.
Please, what am I missing? Thanks for your help.
UPDATE: This works on Xcode 12.2 beta 2 and should work on Xcode 12.0.1
This is the code:
My Model
import Foundation
struct MyModel: Codable {
let value: Double
}
My ViewModel
import Foundation
import SwiftUI
import Combine
final class MyViewModel: ObservableObject {
@Published var state = State.ready
@Published var value: MyModel = MyModel(value: 0.0)
@Published var manualLocation: String {
didSet {
UserDefaults.standard.set(manualLocation, forKey: "manualLocation")
}
}
@EnvironmentObject var coordinates: Coordinates
init() {
manualLocation = UserDefaults.standard.string(forKey: "manualLocation") ?? ""
}
enum State {
case ready
case loading(Cancellable)
case loaded
case error(Error)
}
private var url: URL {
get {
return URL(string: "https://myapi.com&lat=\(coordinates.latitude)&lon=\(coordinates.longitude)")!
}
}
let urlSession = URLSession.shared
var dataTask: AnyPublisher<MyModel, Error> {
self.urlSession
.dataTaskPublisher(for: self.url)
.map { $0.data }
.decode(type: MyModel.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
func load(){
assert(Thread.isMainThread)
self.state = .loading(self.dataTask.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("⚠️ API Call finished")
break
case let .failure(error):
print("❌ API Call failure")
self.state = .error(error)
}
},
receiveValue: { value in
self.state = .loaded
self.value = value
print(" API Call loaded")
}
))
}
}
The Location Manager
import Foundation
import SwiftUI
import Combine
import CoreLocation
import MapKit
final class Coordinates: NSObject, ObservableObject {
@EnvironmentObject var myViewModel: MyViewModel
@Published var latitude: Double = 0.0
@Published var longitude: Double = 0.0
@Published var placemark: CLPlacemark? {
willSet { objectWillChange.send() }
}
private let locationManager = CLLocationManager()
private let geocoder = CLGeocoder()
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
deinit {
locationManager.stopUpdatingLocation()
}
}
extension Coordinates: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
latitude = location.coordinate.latitude
longitude = location.coordinate.longitude
geocoder.reverseGeocodeLocation(location, completionHandler: { (places, error) in
self.placemark = places?[0]
})
self.locationManager.stopUpdatingLocation()
}
}
extension Coordinates {
func getLocation(from address: String, completion: @escaping (_ location: CLLocationCoordinate2D?)-> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks, error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location?.coordinate else {
completion(nil)
return
}
completion(location)
}
}
}
The View
import Foundation
import SwiftUI
struct MyView: View {
@EnvironmentObject var myViewModel: MyViewModel
@EnvironmentObject var coordinates: Coordinates
private var icon: Image { return Image(systemName: "location.fill") }
var body: some View {
VStack{
VStack{
Text("\(icon) \(coordinates.placemark?.locality ?? "Unknown location")")
Text("Latitude: \(coordinates.latitude)")
Text("Longitude: \(coordinates.longitude)")
}
VStack{
Text("UV Index: \(myViewModel.value.value)")
.disableAutocorrection(true)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
HStack{
TextField("Manual location", text: $myViewModel.manualLocation)
if !myViewModel.manualLocation.isEmpty{
Button(action: { clear() }) { Image(systemName: "xmark.circle.fill").foregroundColor(.gray) }
}
}
}.padding()
}
func commit() {
coordinates.getLocation(from: self.myViewModel.manualLocation) { places in
coordinates.latitude = places?.latitude ?? 0.0
coordinates.longitude = places?.longitude ?? 0.0
}
myViewModel.load()
}
func clear() {
myViewModel.manualLocation = ""
myViewModel.load()
}
}