swiftswiftuicore-locationcombineclplacemark

SwiftUI - Get User's coordinates to pass in API call


This problem has been haunting me for months and I believe it comes down to my using the wrong structure and procedure.

I'm trying to do an API call to Yelp's API and passing in the variables for the user's lat/long. I'm able to grab the lat/long based on my current LocationManager, however when it seems as though the lat/long only becomes available AFTER the API call has been made, so the API is getting default 0.0 values for both lat/long.

I'm very much a beginner when it comes to this, but is there a way that I could set up a loading screen that grabs the lat/long in the background and by the time my ExploreView shows, the real location information has been established?

Below is my LocationManager and ExploreView

LocationManager

import Foundation
import CoreLocation

class LocationManager: NSObject, ObservableObject {

private let locationManager = CLLocationManager()
let geoCoder = CLGeocoder()

@Published var location: CLLocation? = nil
@Published var placemark: CLPlacemark? = nil

override init() {
    super.init()
    self.locationManager.delegate = self
    self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
    self.locationManager.distanceFilter = kCLDistanceFilterNone
    self.locationManager.requestWhenInUseAuthorization()
    self.locationManager.startUpdatingLocation()
    
}

func geoCode(with location: CLLocation) {
    geoCoder.reverseGeocodeLocation(location) { (placemark, error) in
        if error != nil {
            print(error!.localizedDescription)
        } else {
            self.placemark = placemark?.first
        }
    }
}

func startUpdating() {
    self.locationManager.delegate = self
    self.locationManager.requestWhenInUseAuthorization()
    self.locationManager.startUpdatingLocation()
    }
}

extension LocationManager: CLLocationManagerDelegate {

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    guard let location = locations.first else {
        return
    }
    self.location = location
    self.geoCode(with: location)
    }
}

ExploreView (The first view that shows upon launch)

import SwiftUI
import CoreLocation
import Foundation


struct ExploreView: View {
    @ObservedObject  var location = LocationManager()
    @ObservedObject var fetcher: RestaurantFetcher


init() {
    let location = LocationManager()
    self.location = location
    self.fetcher = RestaurantFetcher(locationManager: location)
    self.location.startUpdating()
}

var body: some View {
        ScrollView (.vertical) {
            VStack {
                HStack {
                    Text("Discover ")
                        .font(.system(size: 28))
                        .fontWeight(.bold)
                  +  Text(" \(location.placemark?.locality ?? "")")
                        .font(.system(size: 28))
                        .fontWeight(.bold)
                    Spacer()                       
                }
                HStack {
                    SearchBar(text: .constant(""))
                }.padding(.top, 16)                
                HStack {
                    Text("Featured Restaurants")
                        .font(.system(size: 24))
                        .fontWeight(.bold)
                    Spacer()                       
                    NavigationLink(
                        destination: FeaturedView(),
                        label: {
                            Text("View All")
                        })                        
                }.padding(.vertical, 30)                 
                HStack {
                    Text("All Cuisines")
                        .font(.system(size: 24))
                        .fontWeight(.bold)
                    Spacer()
                }                
                Spacer()
            }.padding()
        }      
    }
}

public class RestaurantFetcher: ObservableObject {
    @Published var businesses = [RestaurantResponse]()
    @ObservedObject var locationManager: LocationManager
    let location = LocationManager()

var lat: String {
    return "\(location.location?.coordinate.latitude ?? 0.0)"
}

var long: String {
    return "\(location.location?.coordinate.longitude ?? 0.0)"
}

init(locationManager: LocationManager) {
    let location = LocationManager()
    self.locationManager = location
    self.location.startUpdating()
    
    load()
}

func load() {
    print("\(location.location?.coordinate.latitude ?? 0.0)")
    print("user latitude top of function")
    //Returns default values of 0.0
    let apikey = "APIKEY Here"
    let url = URL(string: "https://api.yelp.com/v3/businesses/search?latitude=\(lat)&longitude=\(long)&radius=40000")!
    var request = URLRequest(url: url)
    request.setValue("Bearer \(apikey)", forHTTPHeaderField: "Authorization")
    request.httpMethod = "GET"
    
    URLSession.shared.dataTask(with: request) { (data, response, error) in
        do {
            if let d = data {
                print("\(self.location.location?.coordinate.longitude ?? 0.0)")
                let decodedLists = try JSONDecoder().decode(BusinessesResponse.self, from: d)
               
                // Returns actual location coordinates
                DispatchQueue.main.async {
                    self.businesses = decodedLists.restaurants
                }
            } else {
                print("No Data")
            }
        } catch {
            print ("Caught")
        }
    }.resume()
    }
}

Solution

  • Try the following modified code (I needed to make some replications, so pay attention - some typos possible).

    The main idea is to subscribe for LocationManager updated location publisher to listen for explicit changes of location and perform next API load only after location is really updated and not nil.

    struct ExploreView: View {
        @ObservedObject  var location: LocationManager
        @ObservedObject var fetcher: RestaurantFetcher
        
        init() {
            let location = LocationManager()  // << use only one instance
            self.location = location
            self.fetcher = RestaurantFetcher(locationManager: location)
            
            self.location.startUpdating()   // << do this only once
        }
        
        var body: some View {
            ScrollView (.vertical) {
                VStack {
                    HStack {
                        Text("Discover ")
                            .font(.system(size: 28))
                            .fontWeight(.bold)
                            +  Text(" \(location.placemark?.locality ?? "")")
                            .font(.system(size: 28))
                            .fontWeight(.bold)
                        Spacer()
                    }
                    HStack {
                        SearchBar(text: .constant(""))
                    }.padding(.top, 16)
                    HStack {
                        Text("Featured Restaurants")
                            .font(.system(size: 24))
                            .fontWeight(.bold)
                        Spacer()
                        NavigationLink(
                            destination: FeaturedView(),
                            label: {
                                Text("View All")
                            })
                    }.padding(.vertical, 30)
                    HStack {
                        Text("All Cuisines")
                            .font(.system(size: 24))
                            .fontWeight(.bold)
                        Spacer()
                    }
                    Spacer()
                }.padding()
            }
        }
    }
    
    import Combine
    
    public class RestaurantFetcher: ObservableObject {
        @Published var businesses = [RestaurantResponse]()
        private var locationManager: LocationManager
        
        var lat: String {
            return "\(locationManager.location?.coordinate.latitude ?? 0.0)"
        }
        
        var long: String {
            return "\(locationManager.location?.coordinate.longitude ?? 0.0)"
        }
        
        private var subscriber: AnyCancellable?
        init(locationManager: LocationManager) {
            self.locationManager = locationManager
            
            // listen for available location explicitly
            subscriber = locationManager.$location
                .debounce(for: 5, scheduler: DispatchQueue.main) // wait for 5 sec to avoid often reload
                .receive(on: DispatchQueue.main)
                .sink { [weak self] location in
                    guard location != nil else { return }
                    self?.load()
                }
        }
        
        func load() {
            print("\(locationManager.location?.coordinate.latitude ?? 0.0)")
            print("user latitude top of function")
            //Returns default values of 0.0
            let apikey = "APIKEY Here"
            let url = URL(string: "https://api.yelp.com/v3/businesses/search?latitude=\(lat)&longitude=\(long)&radius=40000")!
            var request = URLRequest(url: url)
            request.setValue("Bearer \(apikey)", forHTTPHeaderField: "Authorization")
            request.httpMethod = "GET"
            
            URLSession.shared.dataTask(with: request) { (data, response, error) in
                do {
                    if let d = data {
                        print("\(self.locationManager.location?.coordinate.longitude ?? 0.0)")
                        let decodedLists = try JSONDecoder().decode(BusinessesResponse.self, from: d)
                        
                        // Returns actual location coordinates
                        DispatchQueue.main.async {
                            self.businesses = decodedLists.restaurants
                        }
                    } else {
                        print("No Data")
                    }
                } catch {
                    print ("Caught")
                }
            }.resume()
        }
    }