swiftuicllocationcllocationdistance

SwiftUI CLLocationDistance returns a huge incorrect value


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

Solution

  • 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