SwiftUI - Location in API call with CLLocationManager and CLGeocoder

362 views Asked by At

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()
    }
}
0

There are 0 answers