UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Day 78 - coordinates come back as legitimate but are being set to nil

Forums > 100 Days of SwiftUI

I'm working on Day 78 of 100 days of SwiftUI and I've hit a point where I cannot solve the problem I'm having.

I set up a CLLocationManager and requestWhenInUseAuthorization (I have the proper entry in the info group for this) and that all works fine. I set the delegate for it to self.

Then I call requestLocation() on the CLLocationManager I set up. This fires off the didUpdateLocations delegate function. I've got all kinds of print statements in that function that show that it's getting my current location and that I'm saving the results into a variable (lastKnownLocation). All of that code seems to be working fine and the variable gets set properly in that function.

The problem is, after that function returns, lastKnownLocation loses it's value and has a value of nil. The problem shows up in my LocationFetcher class in the start() function after the call to requestLocation. I've tried to solve this for a couple of days but I'm not coming up with anything. I'm hoping some can review my code and spot some mistake I'm making.

There are comments and print statements peppered throughout the code.

Person struct and ViewModel class


import CoreLocation
import SwiftUI

struct Person: Identifiable, Codable, Comparable {
    var id: UUID
    let name: String
    let imageData: Data
    let latitude: Double
    let longitude: Double

    var coordinates: CLLocationCoordinate2D {
        CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
    }

    static func < (lhs: Person, rhs: Person) -> Bool {
        lhs.name < rhs.name
    }
}

extension Person {
    var swiftUIImage: Image {
        if let uiImage = UIImage(data: imageData) {
            return Image(uiImage: uiImage)
        } else {
            return Image(systemName: "person.crop.square")
        }
    }

}

extension ContentView {
    @MainActor class ViewModel: ObservableObject {
        @Published var persons: [Person]

        let savePath = FileManager.documentsDirectory.appendingPathComponent("SavedPersons")

        let locationFetcher = LocationFetcher()
        // create an instance of the LocationFetcher class
        // which has an optional var called lastKnownLocation
        // lastKnownLocation is set inside the locationManager didUpdateLocations

        init() {
            do {
                // use Data() to allow us to use encryption when saving
                let data = try Data(contentsOf: savePath)
                let decodedData = try JSONDecoder().decode([Person].self, from: data)
                persons = decodedData.sorted()
            } catch {
                persons = []
            }

        }

        func getCurrentLocation() -> CLLocationCoordinate2D {
            locationFetcher.start()
            // lastKnownLocation is nil.
            // somehow start is changing the value of lastKnownLocation
            if let lastKnownLocation = locationFetcher.lastKnownLocation {
                print("getCurrentLocation after start() - \(lastKnownLocation)")
            } else {
                print("getCurrentLocation after start() - lastKnownLocation is nil")
            }
            print("getCurrentLocation - lastKnownLocation: \(String(describing: locationFetcher.lastKnownLocation))")
            // at this point lastKnownLocation is set to the default values
            // even though it was written to properly in didUpdateLocations
            if let location = locationFetcher.lastKnownLocation {
                print("getCurrentLocation - Location: \(location)")
                return location.coordinate
            } else {
                // because lastKnownLocation is nil return some default coordinates
                print("getCurrentLocation - Failed to get last known location")
                return CLLocationCoordinate2D(latitude: 50.0, longitude: 50.0)
            }
        }

        func addPerson(name: String, inputImage: UIImage) {
            // convert the UIImage to Data type
            if let imageData = inputImage.jpegData(compressionQuality: 0.6) {
                // entry point for CoreLocation code
                let location = getCurrentLocation()
                print("addPerson after getCurrentLocation - Location: \(location)")

                let newPerson = Person(id: UUID(), name: name, imageData: imageData, latitude: location.latitude, longitude: location.longitude)
                // append newPerson to persons array (in viewModel)
                print("addPerson - newPerson: \(newPerson)")
                // newPerson.latitude/longitude have default values
                persons.append(newPerson)
                save()
            } else {
                print("Failed to convert input image to data")
            }
        }

        func save() {
            do {
                let data = try JSONEncoder().encode(persons)
                // .completeFileProtection means that the file is stored with strong encryption
                try data.write(to: savePath, options: [.atomic, .completeFileProtection])
            } catch {
                print("Unable to save data")
            }
        }
    }
}

and here is the LocationFetcher class


import CoreLocation

class LocationFetcher: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()
    var lastKnownLocation: CLLocation?

    override init() {
        super.init()
        manager.delegate = self
    }

    func start() {
        manager.requestWhenInUseAuthorization()
        print("In start() - called requestWhenInUseAuthorization using manager")

//        manager.startUpdatingLocation()
        manager.requestLocation()
        print("In start() - called requestLocation using manager")
        // after this it will call locationManager()
        print("In start() after requestLocation - manager shows: \(manager.location!.coordinate)")
        // this prints out valid coordinates

        print("In start() - lastKnownLocation: \(String(describing: lastKnownLocation))")
        // this prints that lastKnownLocation is nil
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        // locations might be empty so it needs to be unwrapped
        // the most recent location update is at the end of the array
        if let location = locations.last {
            print("Inside didUpdateLocations - location: \(location)")
            // assign the unwrapped version of location
            // store it in the optional lastKnownLocation
            // locations.last?.coordinate has valid coordinates
            lastKnownLocation = location
            // now lastKnownLocation has valid coordinates
            print("Inside didUpdateLocations - lastKnownLocation: \(lastKnownLocation as Any)")
            // this accurately reflects that lastKnownLocation was set properly
        } else {
            print("Inside didUpdateLocations - Failed to get location coordinates")
            lastKnownLocation = nil
        }
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("Inside didFailWithError - \(error)")
    }
}

2      

manager.requestLocation()
print("In start() - called requestLocation using manager")
// after this it will call locationManager()
print("In start() after requestLocation - manager shows: \(manager.location!.coordinate)")
// this prints out valid coordinates

print("In start() - lastKnownLocation: \(String(describing: lastKnownLocation))")
// this prints that lastKnownLocation is nil

When you print out the value of manager.location!.coordinate you have valid coordinates. Then you print out the value of lastKnownLocation and it's nil. But where do you set the value of lastKnownLocation so that it will have valid coordinates?

You do update lastKnownLocation in the location(_:didUpdateLocations:) delegate method, but that gets called asynchronously, meaning the code where you call manager.requestLocation() will continue running. So lastKnownLocation isn't set by the time the line where you print it out is reached but it may be set later. You need to take that into account and set it yourself based on manager.location.

Try changing the above snippet of code to this:

manager.requestLocation()
print("In start() - called requestLocation using manager")
// after this it will call locationManager()
print("In start() after requestLocation - manager shows: \(manager.location!.coordinate)")
// this prints out valid coordinates

//assign the LocationManager's location property to our
//lastKnownLocation property
lastKnownLocation = manager.location

print("In start() - lastKnownLocation: \(String(describing: lastKnownLocation))")
// this prints that lastKnownLocation is nil

A better option would be to refactor your code so that lastKnownLocation is an @Published property that will automatically update the UI once CLLocationManager does its thing. You would just kick off the request and let SwiftUI handle the updates. Make sure you have something in your UI to handle when lastKnownLocation is nil.

2      

Thanks. That solved it. If I'm not setting lastKnownLocation in didUpdateLocations, what's the purpose of the delegate?

2      

If I'm not setting lastKnownLocation in didUpdateLocations, what's the purpose of the delegate?

Really, you should use the delegate method instead of assigning directly from manager.location. As the docs tell us:

The value of this property is nil if no location data has ever been retrieved.

The docs then go on to explain how location may have older data that is no longer current.

So location may hold a location that isn't correct or even no location at all.

But by updating your lastKnownLocation through the delegate method, you will ensure you get the current user location.

2      

Hacking with Swift is sponsored by Superwall

SPONSORED Superwall lets you build & test paywalls without shipping updates. Run experiments, offer sales, segment users, update locked features and more at the click of button. Best part? It's FREE for up to 250 conversions / mo and the Superwall team builds out 100% custom paywalls – free of charge.

Learn More

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.