Get API Coordinates and Show city in SwiftUI

Viewed 379

I call an API and get a response with a longitude and latitude. Now, I'm trying to show and update the city name. I made a function ("place") to convert the longitude and latitude to a city name. Unfortunately is doesn't work. Please, who could give me a help on this one? Thanks in advance.

Below I will show the code. I filled the struct "APIcoord" with lat and lon example coordinates.

public final class WeatherService: NSObject {
    
    private let locationManager = CLLocationManager()
    private var completionHandler: ((Weather) -> Void)?
    
    public override init() {
        super.init()
        locationManager.delegate = self
    }
    
    public func loadWeatherData(_ completionhandler: @escaping((Weather) -> Void)) {
        self.completionHandler = completionhandler
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
    }
    private func makeDataRequest(forCoordinates coorodinates: CLLocationCoordinate2D) {
        
    }
}

extension WeatherService: CLLocationManagerDelegate {
    public func locationManager(
        _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]
        ) {
            guard let location = locations.first else {return}
            makeDataRequest(forCoordinates: location.coordinate)
            }
    
    
    public func locationManager(
        _ manager: CLLocationManager, didFailWithError error: Error
        ) {
            print("Something went wrong: \(error.localizedDescription)")
            }
}

struct APIResponse: Decodable {
    let coord: APIcoord
}

struct APIcoord: Decodable{
    let lon: Double = -122.031
    let lat: Double = 37.33
}

public struct Weather {

let longtitude: Double
let latitude: Double

init(response: APIResponse) {
    longtitude = response.coord.lon
    latitude = response.coord.lat
}

}

public class WeaterViewModel: ObservableObject {
    @Published var longtitude: Double = 0.0
    @Published var latitude: Double = 0.0
    
    public let weatherService: WeatherService
    
    public init(weatherService: WeatherService) {
        self.weatherService = weatherService
    }
    
    public func refresh() {
        weatherService.loadWeatherData { weather in
            DispatchQueue.main.sync {
                self.longtitude = weather.longtitude
                self.latitude = weather.latitude
            }
        }
    }
}

struct ContentView: View {
    @ObservedObject var viewModel: WeaterViewModel

    @State var placeFound: String?

    var body: some View {
        NavigationView{
         ZStack{
            VStack {
                Text(placeFound ?? "No Place Found")
                    .onAppear(){
                        place(APIlatitude: viewModel.latitude, APIlongitude: viewModel.longtitude){(city) in
                            placeFound = city
                            print("Place: \(String(describing: city))" )
                        }
                    }
            }
            .onAppear{
                viewModel.refresh()
            }
        }
        .navigationBarHidden(true)
        }
    }
    
    func place(APIlatitude : Double, APIlongitude : Double, completion: @escaping (String?) -> Void)  {
        let geoCoder = CLGeocoder()
        let location = CLLocation(latitude: APIlatitude, longitude: APIlongitude)
        
           geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, _) -> Void in
    
               placemarks?.forEach { (placemark) in
    
                   if let city = placemark.locality {
                       print(city)
                       completion(city)
                   }
                   else{
                       print("Can't find place")
                   }
               }
           })
    }
}
1 Answers

You could pair GLGeocoder's async API with SwiftUI's task like this:

struct ContentView: View {
    @StateObject var locator = Locator()
    
    var body: some View {
        if let location = locator.location {
            ContentView2(location: location)
        }
        else {
            Text("Hello, world!")
                .padding()
        }
    }
}


struct ContentView2Config {
    let geocoder = CLGeocoder()
    var text = Self.noCityText
    static let noCityText = "No City"
    
    mutating func reverseGeocode(location: Location) async {
        text = Self.noCityText
        let location = CLLocation(latitude: location.lat, longitude: location.lon)
        if let city = try? await geocoder.reverseGeocodeLocation(location)
            .first
            .flatMap({ placemark in
                placemark.locality
            })
        {
            text = city
        }
        else {
            text = Self.noCityText
        }
    }
}

struct ContentView2: View {
    let location: Location
    @State var config = ContentView2Config()
    
    var body: some View {
        Text(config.text)
        .task(id: location.id) {
            await config.reverseGeocode(location: location)
        }
    }
}

And in case it helps, here is the location manager:

struct Location: Identifiable {
    let id = UUID()
    let lat: Double
    let lon: Double
}

class Locator: NSObject, ObservableObject, CLLocationManagerDelegate {
    @Published var location: Location?
    let locationManager = CLLocationManager()
    
    override init() {
        super.init()
        locationManager.delegate = self
        locationManager.startUpdatingLocation()
        locationManager.requestWhenInUseAuthorization()
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let coordinate = locations.last?.coordinate else {
            location = nil
            return
        }
        location = Location(lat: coordinate.latitude, lon: coordinate.longitude)
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        location = nil
    }
}
Related