First, I am a very novice coder learning as I make my app, so the app is probably comprised of unnecessary and unconventional code. Apologies in advance. Because of my lack of coding knowledge I would like to fix this while making as few changes to the code as possible as to not require me to rewrite large chunks of the app.
I am using CLLocation to find the distance between two points in a golf app to track shot distances. When I print the locations to console, the coordinates point to the correct locations, but when I find the distance between the two points the value returned is huge even though both coordinates are actually very close together.
In this DetailView, pressing the "Swing Location" button grabs the user location and sets that as shotCoord, then the button becomes "Ball Location." When "Ball Location" is pressed its grabs the location again and sets it to ballCoord, then prints the distance between ballCoord and shotCoord. I am coding on Swift Playgrounds for iPad and using location services so the location grabbed is my actual device location.
struct ClubDetailView: View {
@ObservedObject var club: Club
@Environment(\.managedObjectContext) private var viewContext
@StateObject var locationManager = LocationManagerModel()
@State var newShot: Int = 0
@Binding var waiting: Bool
@Binding var shotCoord: CLLocation
@Binding var ballCoord: CLLocation
var body: some View{
List {
Section {
Text(self.club.name)
Text("Average distance: \(club.yardsNum) yards")
Text("\(club.strokes) Strokes Counted")
}
Section{
TextField("", value: $newShot, format: .number)
.onSubmit {
do {
addNewShot(newShot: newShot)
try viewContext.save()
} catch {
print("error")
}
}
}
}
if self.waiting == false{
Button(action: {
let shotCoords = getShotLocation()
print("shotCoords = \(shotCoords)")
self.waiting = true
}, label: {
Text("Swing Location")
.foregroundColor(.white)
.font(.system(.title, design: .rounded, weight: .bold))
.frame(maxWidth: .infinity)
})
.buttonStyle(.borderedProminent)
}
if self.waiting == true{
Button(action: {
let ballCoords = getBallLocation()
//print("ballCoord = \(ballCoords)")
let distance = ballCoords.distance(from: shotCoord)
print(distance)
self.waiting = false
}, label: {
Text("Ball Location")
.foregroundColor(.white)
.font(.system(.title, design: .rounded, weight: .bold))
.frame(maxWidth: .infinity)
})
.buttonStyle(.borderedProminent)
}
}
private func addNewShot(newShot: Int) -> Void {
let newShot = newShot
let avgYards = club.yardsNum * club.strokes
club.strokes += 1
club.yardsNum = (avgYards + newShot) / club.strokes
}
func getShotLocation() -> CLLocation {
locationManager.requestAllowOnceLocationPermission()
let shotCoord = locationManager.lastLocation
return shotCoord!
//print(shotCoords as Any)
}
func getBallLocation() -> CLLocation {
locationManager.requestAllowOnceLocationPermission()
let ballCoords = locationManager.lastLocation
//let distance = ballCoords?.distance(from: shotCoord)
return ballCoords!
//print(ballCoords as Any)
}
}
This code results in shotCoords and ballCoords being printed to the console with my actual current location, but distance returns "9582674.806193907". Since I'm sitting in one place while grabbing both locations, and the coordinates in console are the same down to the last 4 digits, distance should actually be close to 0.
Here is my LocationManager
final class LocationManagerModel: NSObject, ObservableObject, CLLocationManagerDelegate {
@Published var locationStatus: CLAuthorizationStatus?
@Published var lastLocation: CLLocation?
let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
func requestAllowOnceLocationPermission() {
locationManager.requestLocation()
locationManager.stopUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let latestLocation = locations.first else {
print("error")
return
}
DispatchQueue.main.async {
self.lastLocation = latestLocation
}
//self.location = lastLocation?.coordinate
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
}
There is a basic mistake. Your LocationManagerModel
cannot work because receiving locations works asynchronously. Reading lastLocation
right after calling requestAllowOnceLocationPermission
will never show the requested location, because it takes some time until didUpdateLocations
is called and the location is updated.
I recommend to declare shotCoord
and ballCoord
inside LocationManagerModel
as @Published
to get notified when the location is available. You can distinguish ball
and shot
with an enum. The code handles also the authorization. Don't forget to set the NSLocationWhenInUseUsageDescription
key in Info.plist
.
If you want to call requestLocation()
to get a location once do not call startUpdatingLocation()
and stopUpdatingLocation()
simultaneously.
With the second pair of @Published
properties you can show an error in the view
class LocationManagerModel : NSObject, ObservableObject {
enum LocationMode {
case ball, shot
}
@Published var shotCoord : CLLocationCoordinate2D?
@Published var ballCoord : CLLocationCoordinate2D?
@Published var errorMessage = ""
@Published var showError = false
private let locationManager = CLLocationManager()
private var mode : LocationMode = .ball
public override init() {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
}
public func currentLocation(mode: LocationMode) {
self.mode = mode
locationManager.requestLocation()
}
}
extension LocationManagerModel : CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
errorMessage = error.localizedDescription
showError = true
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
switch mode {
case .ball:
ballCoord = locations.last!.coordinate
case .shot:
shotCoord = locations.last!.coordinate
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .authorizedAlways: print("authorizedAlways")
case .authorizedWhenInUse: print("authorizedWhenInUse")
case .denied, .restricted: print("denied")
// show the error to the user
case .notDetermined: print("notDetermined")
locationManager.requestWhenInUseAuthorization()
@unknown default: print("This should never appear")
}
}
}
The view is very simple, it contains only the two buttons to get the locations
struct ContentView: View {
@StateObject var locationManager = LocationManagerModel()
var body: some View {
VStack {
Button(action: {locationManager.currentLocation(mode: .ball)}) {
Text("Ball")
}
Text("lat: \(locationManager.ballCoord?.latitude ?? 0.0) - lng: \(locationManager.ballCoord?.longitude ?? 0.0)")
Button(action: {locationManager.currentLocation(mode: .shot)}) {
Text("Shot")
}
Text("lat: \(locationManager.shotCoord?.latitude ?? 0.0) - lng: \(locationManager.shotCoord?.longitude ?? 0.0)")
}
}
}
The print
lines in locationManagerDidChangeAuthorization
are only for debugging purposes. Actually you need only notDetermined
to call requestWhenInUseAuthorization()
and denied
/restricted
to show an error