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