Search code examples
swiftrealmcllocationmanagernsoperationqueue

how to use CLLocation delegates in NSOperation with Realm in swift?


I'm making use of CLLocation to determine the user's place in PageViewController, and need to use the returned location from locationManager(_:didUpdateLocations:) writing to Realm database, then using the Realm in my function in PageViewController's content page.The content page is implemented in a separate source file(view controller). so:

first, locating success,

second, write the location to realm,

third, after the second step success, call the function in content page.

And I use the NSOperationQueue with dependencies to control above steps, but the delegate function locationManager(_:didUpdateLocations:) seems never called, and cause the app crash when the code want to read the realm data:

Pls see following my code:

PageViewController

class PageViewController: UIPageViewController {
   static var isFirstLaunch: Bool = true
   let locationManager: CLLocationManager = CLLocationManager()

 override func viewDidLoad() {
    super.viewDidLoad()

    locationManager.delegate = self
    locationManager.requestWhenInUseAuthorization()
    locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
    locationManager.distanceFilter = 3000

    launchQueue = OperationQueue()

    let locationOperation = BlockOperation {
        OperationQueue.main.addOperation {
            self.locationManager.requestLocation()
        }

    }
    locationOperation.completionBlock = {
        print("locationOperation finished, finished:\(locationOperation.isFinished)") //"locationOperation finished, finished:true" in console
    }

    let firstLaunchOperation = BlockOperation {
        PageViewController.isFirstLaunch = false
    }
    firstLaunchOperation.completionBlock = {
        print("firstLaunchOperation finished, finished:\(firstLaunchOperation.isFinished)") //"firstLaunchOperation finished, finished:true" in console
    }

    firstLaunchOperation.addDependency(locationOperation)

    launchQueue.addOperation(locationOperation)
    launchQueue.addOperation(firstLaunchOperation)
    }

ContentViewController(in a separate source file)

class ContentViewController: UIViewController {
   let defaultRealm = try! Realm()
   let config = Realm.Configuration(fileURL: Bundle.main.url(forResource: "areaID", withExtension: "realm"), readOnly: true)

override func viewDidLoad() {
    super.viewDidLoad()

    UISetup()
    autolayoutView()

    if !PageViewController.isFirstLaunch {
            upateWeather()
    }
}

func upateWeather() {
    let userArea = defaultRealm.objects(UserArea.self)
    let place = userArea.first?.areas
    let locality = place?[currentPage].locality
    let subLocality = place?[currentPage].subLocality
    let areaIDRealm = try! Realm(configuration: config)

    let results = areaIDRealm.objects(RealmObject.self).filter("locality = '\(locality!)' AND subLocality = '\(subLocality!)'")  
    //Crashed here! fatal error: unexpectedly found nil while unwrapping an Optional value. I opened the realm, and no locality and subLocality write in the Realm.

}

}

CLLocationManagerDelegate

extension PageViewController: CLLocationManagerDelegate {

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let currentLocation: CLLocation = locations[0]
let geocoder: CLGeocoder = CLGeocoder()

if currentLocation.horizontalAccuracy > 0 {
    geocoder.reverseGeocodeLocation(currentLocation, completionHandler: {(placeMarks, error) in
        if error == nil {
            guard let placemark = placeMarks!.first else { return }

            let userArea = self.defaultRealm.objects(UserArea.self)

            func locationToRealm(place: String, subPlace: String) {
                if let gpsLocation = userArea.first?.areas.first {
                    self.defaultRealm.beginWrite()
                    gpsLocation.locality = place
                    gpsLocation.subLocality = subPlace
                    try! self.defaultRealm.commitWrite()
                } else {
                    try! self.defaultRealm.write {
                        self.defaultRealm.create(UserArea.self, value: [[["locality": place, "subLocality": subPlace]]])
                    }
                }
            }

            if let place: String = placemark.locality {
                if let subPlace: String = placemark.subLocality {
                    locationToRealm(place: place, subPlace: subPlace)
                } else {
                    locationToRealm(place: place, subPlace: "---")
                }
            } else {
                if let subPlace: String = placemark.subLocality {
                    locationToRealm(place: "---", subPlace: subPlace)
                }
            }
        }
    })
}
}

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {......}

}

How can I use NSOperationQueue to control the code, make them step by step! Thank you very much for the help!


Solution

  • Okay, I've examined the logic and I think I can see what's happening.

    There's no point in placing self.locationManager.requestLocation() in an operation queue (especially on the main queue). According to the documentation, requestLocation() returns immediately, and the actual request to Location Services happens in a background thread controlled by the system.

    When Location Services has determined the device location, the delegate method didUpdateLocations is called, but this can easily take up to several seconds.

    In the meantime, you're loading ContentViewController instantly, and this is triggering updateWeather() before Location Services has had a chance to call the delegate and insert the data into Realm for the first time.

    This is a completely normal logic model. It would make sense to show the view controller with some kind of loading spinner, and to subsequently update it after Location Services has completed. As such, you should make sure that updateWeather() is able to work, even if the Realm file is empty, and you should also incorporate logic to update the UI once the Realm file does actually have data.

    The easiest thing to do would be to check that locality and subLocality aren't nil and only performing the query when they aren't.

    func upateWeather() {
        let userArea = defaultRealm.objects(UserArea.self)
        let place = userArea.first?.areas
        let locality = place?[currentPage].locality
        let subLocality = place?[currentPage].subLocality
        let areaIDRealm = try! Realm(configuration: config)
    
        // Only proceed if the Realm query variables aren't nil
        guard let locality = locality, subLocality = subLocality else {
           return
        }
    
        let results = areaIDRealm.objects(RealmObject.self).filter("locality = '\(locality!)' AND subLocality = '\(subLocality!)'")  
        //Crashed here! fatal error: unexpectedly found nil while unwrapping an Optional value. I opened the realm, and no locality and subLocality write in the Realm.
    
    }
    

    You'll then need to call updateWeather() again once the delegate has completed writing to Realm to try again.