CloudKit - How to modify the record in the background - ios

My App can use location updates in the background now.
Then, can we modify the record in the background when a user is moving?
Code:
#IBOutlet weak var mapView: MKMapView!
var locationManager: CLLocationManager!
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()
locationManager.allowsBackgroundLocationUpdates = true
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {
recordLocation()
}
func recordLocation() {
let publicDatabase = CKContainer.default().publicCloudDatabase
let predicate = NSPredicate(format: "accountID == %#", argumentArray: [myID!])
let query = CKQuery(recordType: "Accounts", predicate: predicate)
publicDatabase.perform(query, inZoneWith: nil, completionHandler: {(records, error) in
if let error = error {
print("error1: \(error)")
return
}
for record in records! {
record["currentLocation"] = self.mapView.userLocation.location
publicDatabase.save(record, completionHandler: {(record, error) in
if let error = error {
print("error2: \(error)")
return
}
print("success!")
})
}
})
}
image - Capability of Background Modes
By the way, everything is fine as long as my app runs in the foreground.
Versions
Xcode 12.2 / Swift 4.2
Summary
Could you please tell me how to modify the record in the background? In the first place, can we do that?
Thanks.

I misunderstood. I had already recorded my location in the background, but had failed to get the latest one.
record["currentLocation"] = self.mapView.userLocation.location
By using this code, I had recorded the location the last moment that I were viewing MKMapView.
In other words, the problem, recording in the background, have been solved from the beginning.
I rewrote as the following code:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {
if let location = locations.first {
recordLocation(currentLocation: location)
}
}
func recordLocation(currentLocation: CLLocation) {
let publicDatabase = CKContainer.default().publicCloudDatabase
let predicate = NSPredicate(format: "accountID == %#", argumentArray: [myID!])
let query = CKQuery(recordType: "Accounts", predicate: predicate)
publicDatabase.perform(query, inZoneWith: nil, completionHandler: {(records, error) in
if let error = error {
print("error1: \(error)")
return
}
for record in records! {
record["currentLocation"] = currentLocation as CLLocation
publicDatabase.save(record, completionHandler: {(record, error) in
if let error = error {
print("error2: \(error)")
return
}
print("success!: \(String(describing: currentLocation))")
})
}
})
}

Related

Track changes in the location for a certain distance iOS

I have a task to track the user's location in the background afterwards, and if its location has changed to more than 5 miles, then I need to update this data on the server. I know that you can start tracking user locations using startMonitoringSignificantLocationChanges. I started testing, launched the application with startMonitoringSignificantLocationChanges and allowsBackgroundLocationUpdates = true, then removed the application from the simulator memory, went into Maps and enabled Free Way simulation. For a minute I got 8 updates on the server, for me it's too often. I think for me, the best solution was if we ask what distance we want to receive updates from. I read a few posts about this, but not one did not solve my problem. I also thought that you can save the previous location and compare the changes with the new location, but I think this is a bad idea. Tell me, how to solve this problem better?
class LocationManager: NSObject {
private override init() {
super.init()
}
static let shared = LocationManager()
private let locationManager = CLLocationManager()
weak var delegate: LocationManagerDelegate?
// MARK: - Flags
private var isCallDidStartGetLocation = false
// MARK: - Measuring properties
private var startTimestamp = 0.0
// MARK: - Open data
var currentLocation: CLLocation?
// MARK: - Managers
private let locationDatabaseManager = LocationDatabaseManager()
// MARK: - Values
private let metersPerMile = 1609.34
func start() {
// measuring data
startTimestamp = Date().currentTimestamp
FirebasePerformanceManager.shared.getUserLocation(true)
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.activityType = .other
locationManager.distanceFilter = 100
locationManager.delegate = self
let status = CLLocationManager.authorizationStatus()
switch status {
case .authorizedAlways:
locationManager.startUpdatingLocation()
case .authorizedWhenInUse:
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()
case .restricted, .notDetermined:
locationManager.requestAlwaysAuthorization()
case .denied:
showNoPermissionsAlert()
}
}
func logOut() {
locationManager.stopUpdatingLocation()
isCallDidStartGetLocation = false
}
}
// MARK: - Alerts
extension LocationManager {
private func showNoPermissionsAlert() {
guard let topViewController = UIApplication.topViewController() else { return }
let alertController = UIAlertController(title: "No permission",
message: "In order to work, app needs your location", preferredStyle: .alert)
let openSettings = UIAlertAction(title: "Open settings", style: .default, handler: {
(action) -> Void in
guard let URL = Foundation.URL(string: UIApplicationOpenSettingsURLString) else { return }
UIApplication.shared.open(URL, options: [:], completionHandler: nil)
})
alertController.addAction(openSettings)
topViewController.present(alertController, animated: true, completion: nil)
}
}
// MARK: - CLLocationManager Delegate
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .authorizedWhenInUse, .authorizedAlways:
locationManager.startUpdatingLocation()
default: break
}
delegate?.didChangeAuthorization?(manager: manager, didChangeAuthorization: status)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let lastLocation = locations.last else { return }
let timeInterval = abs(lastLocation.timestamp.timeIntervalSinceNow)
guard timeInterval < 60 else { return }
currentLocation = lastLocation
locationDatabaseManager.updateUserLocation(lastLocation)
measureGetLocationTime()
if !isCallDidStartGetLocation {
isCallDidStartGetLocation = true
delegate?.didStartGetLocation?()
}
}
}
// MARK: - Calculation
extension LocationManager {
func calculateDistanceFromCurrentLocation(_ venueLocation: CLLocation) -> Double {
guard let userLocation = locationManager.location else {
return 0.0
}
let distance = userLocation.distance(from: venueLocation)
let distanceMiles = distance / DistanceConvertor.metersPerMile //1609
return distanceMiles.roundToPlaces(places: 1)
}
}
// MARK: - Measuring functions
extension LocationManager {
private func measureGetLocationTime() {
FirebasePerformanceManager.shared.getUserLocation(false)
let endTimestamp = Date().currentTimestamp
let resultTimestamp = endTimestamp - startTimestamp
BugfenderManager.getFirstUserLocation(resultTimestamp)
}
}
I changed the current LocationManager and created two new managers for this case. I tested the application, after my changes and the results are as follows: I drove 120-130 km, two segments of the way were between cities, the application spent 1% of the device's charge, for us this is an acceptable result. The App sent 4 requests to the server with the update of the user's location, the conditions were as follows: after the previous update the location took 2 hours and the distance between the previous and the new location was 5 or more miles. You can see the implementation below.
LocationManager
import Foundation
import CoreLocation
class LocationManager: NSObject {
private override init() {
super.init()
manager.delegate = self
}
static let shared = LocationManager()
private let manager = CLLocationManager()
weak var delegate: LocationManagerDelegate?
// MARK: - Enums
enum DistanceValue: Int {
case meters, miles
}
// MARK: - Flags
private var isCallDidStartGetLocation = false
// MARK: - Measuring properties
private var startTimestamp = 0.0
// MARK: - Open data
var currentLocation: CLLocation?
// MARK: - Managers
private let locationDatabaseManager = LocationDatabaseManager()
// MARK: - Values
private let metersPerMile = 1609.34
func start() {
// measuring data
startTimestamp = Date().currentTimestamp
FirebasePerformanceManager.shared.getUserLocation(true)
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
manager.activityType = .other
manager.desiredAccuracy = 45
manager.distanceFilter = 100
let status = CLLocationManager.authorizationStatus()
switch status {
case .authorizedAlways:
if UIApplication.shared.applicationState != .background {
manager.startUpdatingLocation()
}
manager.startMonitoringSignificantLocationChanges()
manager.allowsBackgroundLocationUpdates = true
case .authorizedWhenInUse:
manager.requestAlwaysAuthorization()
manager.startUpdatingLocation()
case .restricted, .notDetermined:
manager.requestAlwaysAuthorization()
case .denied:
showNoPermissionsAlert()
}
}
func logOut() {
manager.stopUpdatingLocation()
isCallDidStartGetLocation = false
}
}
// MARK: - Mode managing
extension LocationManager {
open func enterBackground() {
manager.stopUpdatingLocation()
manager.startMonitoringSignificantLocationChanges()
}
open func enterForeground() {
manager.startUpdatingLocation()
}
}
// MARK: - Alerts
extension LocationManager {
private func showNoPermissionsAlert() {
guard let topViewController = UIApplication.topViewController() else { return }
let alertController = UIAlertController(title: "No permission",
message: "In order to work, app needs your location", preferredStyle: .alert)
let openSettings = UIAlertAction(title: "Open settings", style: .default, handler: {
(action) -> Void in
guard let URL = Foundation.URL(string: UIApplicationOpenSettingsURLString) else { return }
UIApplication.shared.open(URL, options: [:], completionHandler: nil)
})
alertController.addAction(openSettings)
topViewController.present(alertController, animated: true, completion: nil)
}
}
// MARK: - CLLocationManager Delegate
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .authorizedWhenInUse, .authorizedAlways:
if UIApplication.shared.applicationState != .background {
manager.startUpdatingLocation()
}
default: break
}
delegate?.didChangeAuthorization?(manager: manager, didChangeAuthorization: status)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let lastLocation = locations.last else { return }
let applicationState = UIApplication.shared.applicationState
switch applicationState {
case .active, .inactive:
activeAppGetLocation(lastLocation)
case .background:
backgroundAppGetLocation(lastLocation)
}
}
}
// MARK: - Gettings location functions
extension LocationManager {
private func activeAppGetLocation(_ location: CLLocation) {
let timeInterval = abs(location.timestamp.timeIntervalSinceNow)
guard timeInterval < 60 else { return }
currentLocation = location
locationDatabaseManager.updateUserLocation(location, state: .active)
if !isCallDidStartGetLocation {
measureGetLocationTime()
isCallDidStartGetLocation = true
delegate?.didStartGetLocation?()
}
}
private func backgroundAppGetLocation(_ location: CLLocation) {
let locationBackgroundManager = LocationBackgroundManager()
locationBackgroundManager.updateLocationInBackgroundIfNeeded(location)
}
}
// MARK: - Calculation
extension LocationManager {
func calculateDistanceBetweenLocations(_ firstLocation: CLLocation, secondLocation: CLLocation, valueType: DistanceValue) -> Double {
let meters = firstLocation.distance(from: secondLocation)
switch valueType {
case .meters:
return meters
case .miles:
let miles = meters / DistanceConvertor.metersPerMile
return miles
}
}
/// In miles
func calculateDistanceFromCurrentLocation(_ venueLocation: CLLocation) -> Double {
guard let userLocation = manager.location else {
return 0.0
}
let distance = userLocation.distance(from: venueLocation)
let distanceMiles = distance / DistanceConvertor.metersPerMile //1609
return distanceMiles.roundToPlaces(places: 1)
}
}
// MARK: - Measuring functions
extension LocationManager {
private func measureGetLocationTime() {
FirebasePerformanceManager.shared.getUserLocation(false)
let endTimestamp = Date().currentTimestamp
let resultTimestamp = endTimestamp - startTimestamp
BugfenderManager.getFirstUserLocation(resultTimestamp)
}
}
LocationBackgroundManager
import Foundation
import CoreLocation
import SwiftDate
class LocationBackgroundManager {
private var backgroundLocationUpdateTimestamp: Double {
get {
return UserDefaults.standard.double(forKey: "backgroundLocationUpdateTimestamp")
}
set {
UserDefaults.standard.set(newValue, forKey: "backgroundLocationUpdateTimestamp")
UserDefaults.standard.synchronize()
}
}
// MARK: - Managers
private lazy var locationStorageManager: LocationStorageManager = {
let locationStorageManager = LocationStorageManager()
return locationStorageManager
}()
open func updateLocationInBackgroundIfNeeded(_ location: CLLocation) {
if backgroundLocationUpdateTimestamp != 0 {
let currentLocationDate = location.timestamp
let previousDate = Date(timeIntervalSince1970: backgroundLocationUpdateTimestamp)
guard let hours = (currentLocationDate - previousDate).in(.hour) else { return }
guard hours >= 2 else { return }
if let previousLocationRealm = locationStorageManager.getCurrentUserPreviousLocation() {
let previousLocation = CLLocation(latitude: previousLocationRealm.latitude, longitude: previousLocationRealm.longitude)
let distance = LocationManager.shared.calculateDistanceBetweenLocations(location, secondLocation: previousLocation, valueType: .miles)
guard distance >= 5 else { return }
updateLocation(location)
} else {
updateLocation(location)
}
} else {
updateLocation(location)
}
}
private func updateLocation(_ location: CLLocation) {
let locationDatabaseManager = LocationDatabaseManager()
locationDatabaseManager.updateUserLocation(location, state: .background)
backgroundLocationUpdateTimestamp = location.timestamp.currentTimestamp
locationStorageManager.saveLocation(location)
}
}
LocationStorageManager
import Foundation
import CoreLocation
import RealmSwift
class LocationStorageManager {
func saveLocation(_ location: CLLocation) {
guard let currentUserID = RealmManager().getCurrentUser()?.id else { return }
let altitude = location.altitude
let latitude = location.coordinate.latitude
let longitude = location.coordinate.longitude
let locationRealm = LocationRealm(altitude: altitude, latitude: latitude, longitude: longitude, userID: currentUserID)
do {
let realm = try Realm()
try realm.write {
realm.add(locationRealm, update: true)
}
} catch {
debugPrint(error)
let funcName = #function
let file = #file
BugfenderManager.reportError(funcName, fileName: file, error: error)
}
}
func getCurrentUserPreviousLocation() -> LocationRealm? {
guard let currentUserID = RealmManager().getCurrentUser()?.id else { return nil }
do {
let realm = try Realm()
let previousLocation = realm.objects(LocationRealm.self).filter("userID == %#", currentUserID).first
return previousLocation
} catch {
debugPrint(error)
let funcName = #function
let file = #file
BugfenderManager.reportError(funcName, fileName: file, error: error)
return nil
}
}
}
According to Apple Docs:
Apps can expect a notification as soon as the device moves 500 meters or more from its previous notification. It should not expect notifications more frequently than once every five minutes. If the device is able to retrieve data from the network, the location manager is much more likely to deliver notifications in a timely manner.
startMonitoringSignificantLocationChanges() is the least accurate way to monitor location and there is no way to configure how often it's called as it's triggered in the event of a cell tower transition. Therefore it can trigger more often in areas with more densely located tower (cities). See this thread for more information.

CLLocationManager make my app crash having location activated

It's weird. There are some devices that crash and some other devices that not. The thing is when having location not activated the app never dies but when I allow my app access to the location in some devices crash and in other devices not.
This is the code:
override func viewDidAppear(animated: Bool) {
if CLLocationManager.locationServicesEnabled(){
switch CLLocationManager.authorizationStatus() {
case .NotDetermined, .Restricted, .Denied:
print("No access")
case .AuthorizedAlways, .AuthorizedWhenInUse:
let geocoder = CLGeocoder()
longitude = self.locationManager.location!.coordinate.longitude
latitude = self.locationManager.location!.coordinate.latitude
geocoder.reverseGeocodeLocation(CLLocation(latitude: (latitude), longitude: (longitude)), completionHandler: {placemarks, error in
if error == nil && placemarks!.count > 0 {
self.thoroughfare = (placemarks!.last?.thoroughfare)!
self.city = (placemarks!.last?.locality)!
print(self.thoroughfare)
print(self.city)
print(self.longitude)
print(self.latitude)
}
})
}
} else {
print("Location services are not enabled")
}
}
When app crashes the error points to this line:
longitude = self.locationManager.location!.coordinate.longitude
latitude = self.locationManager.location!.coordinate.latitude
I've tested the app in 10 devices, having 1-2 of them that crashes at this point.
What's happening? I think I'm managing rightly what to do and what no to do when location is or not is allowed.
You should chek if
self.locationManager.location
Is null before using it
Please try this whole code to get the location and its details.Its tried and working solution in Swift 3.0
import CoreLocation
import Foundation
class ViewController: UIViewController,CLLocationManagerDelegate {
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
findMyLocation()
}
func findMyLocation(){
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
CLGeocoder().reverseGeocodeLocation(manager.location!, completionHandler: {(placemarks, error)->Void in
if (error != nil) {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
if placemarks!.count > 0 {
let pm = placemarks![0]
self.displayLocationInfo(pm)
} else {
print("Problem with the data received from geocoder")
}
})
}
func displayLocationInfo(_ placemark: CLPlacemark?) {
if let containsPlacemark = placemark {
//stop updating location to save battery life
locationManager.stopUpdatingLocation()
let locality = (containsPlacemark.locality != nil) ? containsPlacemark.locality : ""
let postalCode = (containsPlacemark.postalCode != nil) ? containsPlacemark.postalCode : ""
let administrativeArea = (containsPlacemark.administrativeArea != nil) ? containsPlacemark.administrativeArea : ""
let country = (containsPlacemark.country != nil) ? containsPlacemark.country : ""
print(" Postal Code \(postalCode)")
print(" administrativeArea \(administrativeArea)")
print(" country \(country)")
print(" locality \(locality)")
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Error while updating location " + error.localizedDescription)
}
Thank you
Don't declare optional values to variable. always handle errors
Don't unwrap the location
self.locationManager.location // Your error
if var longitude = self.locationManager.location.coordinate.longitude {
// do your thing
}else {
// handle the error by declaring default value
}
second thing you also might receive null values even if user lost internet while getting the location or you forget the simulate the location while testing in simulator so always handle the error
Please Check your didUpdateLocations method, you just need to check whether location is getting correctly or getting nil.
if ((self.locationManager.location) != nil){
//Get Location access here.
}

Refreshing location then running NSXMLParser

My app (1) gets the user's location then (2) parses XML based on that location data. From load, the app works great. But I would like to get updated XML based on a change of location when the user taps the refresh button. I've tried several versions of this but can't get to work. I've included the portion of my code I think is relevant to this question (I think it's a timing issue). On tapping the refresh button, the location updates but the old XML is loaded:
class Myclass: UIPageViewController, UIPageViewControllerDataSource, CLLocationManagerDelegate, NSXMLParserDelegate {
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.requestWhenInUseAuthorization()
//locationManager.requestLocation()
}
override func viewWillAppear(animated: Bool) {
switch CLLocationManager.authorizationStatus() {
case .AuthorizedWhenInUse, .AuthorizedAlways:
busyAlertController.display()
locationManager.requestLocation()
print("Authorized")
case .NotDetermined:
locationManager.requestWhenInUseAuthorization() // or request always if you need it
print("Not Determined")
case .Restricted, .Denied:
print("Restricted or Denied")
self.dismissViewControllerAnimated(true, completion: nil)
let alertController = UIAlertController(
title: "Background Location Access Disabled",
message: "We need to know your location to show you the correct forecast, please open this app's settings and set location access to 'When in Use' or 'Always'.",
preferredStyle: .Alert)
let cancelAction = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
alertController.addAction(cancelAction)
let openAction = UIAlertAction(title: "Open Settings", style: .Default) { (action) in
if let url = NSURL(string:UIApplicationOpenSettingsURLString) {
UIApplication.sharedApplication().openURL(url)
}
}
alertController.addAction(openAction)
self.presentViewController(alertController, animated: true, completion: nil)
}
}
// MARK: UIPageViewControllerDataSource & UIPageViewControllerDelegate
// MARK: - CLLocationManagerDelegate
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
if (status == .AuthorizedAlways) || (status == .AuthorizedWhenInUse) {
locationManager.requestLocation()
}
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
let lat = "\(location.coordinate.latitude)"
let lon = "\(location.coordinate.longitude)"
let url = baseURL + lat + "&lon=" + lon + suffixURL
guard let urlAsNSURL = NSURL(string: url) else {return}
NWSURL = urlAsNSURL
runParser()
} else {
//TODO:
}
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
print("Error finding location: \(error.localizedDescription)")
showAlert("Location Problem", message: "We're having trouble finding your location, please try again.")
}
//XMLParser Methods
func parserDidEndDocument(parser: NSXMLParser){
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.showVC()
})
}
func runParser() {
guard let url = URL else {
return}
guard let parser = NSXMLParser(contentsOfURL: url) else {return}
parser.delegate = self
parser.parse()
}
#IBAction func refresh(sender: UIBarButtonItem) {
locationManager.requestLocation()
//runParser()
}
}
The locations array which was passed into locationManager:didUpdateLocations: may contain more than one location in case updates were deferred or multiple locations arrived before they could be delivered.
Since it is organized in the order in which the updates occurred, the most recent location update is at the end of the array.
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
...
}
}
The problem was that I wasn't clearing out a variable (array) after the NSXMLParser was done, and so I was appending over the stale data, yet since the stale data was first it was displaying in my UI and made it VERY hard to detect the problem until I printed to the console and saw the multiple arrays. I've done something similar before so note to anyone implementing NSXMLParser: make sure that you clear out the variable that you are using to store data in didEndElement.

ApplicationdidEnterBackground swift

here is my class OneShotLocationManager that gives the location for the user
when calling it from the ViewController it is working fine but when calling it from the ApplicationdidEnterBackground it is not working but but displaying each 0.4 UpdateLocation in the console !
func applicationDidEnterBackground(application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
println("entered background")
var timer = NSTimer.scheduledTimerWithTimeInterval(0.4, target: self, selector: Selector("updateLocation"), userInfo: nil, repeats: true)
}
func updateLocation(){
// start location services, here
println("updateLocation")
var manager: OneShotLocationManager?
manager = OneShotLocationManager()
manager!.fetchWithCompletion {location, error in
// fetch location or an error
if let loc = location {
} else if let err = error {
println(err.localizedDescription)
}
println("end up")
manager = nil
}
}
override func viewDidLoad() {
super.viewDidLoad()
ViewController.UpdateLocation()
}
class func UpdateLocation(){
var manager: OneShotLocationManager?
manager = OneShotLocationManager()
manager!.fetchWithCompletion {location, error in
// fetch location or an error
if let loc = location {
println(loc.description)
} else if let err = error {
println(err.localizedDescription)
}
manager = nil
}
}
class OneShotLocationManager: NSObject, CLLocationManagerDelegate {
//location manager
private var locationManager: CLLocationManager?
//destroy the manager
deinit {
locationManager?.delegate = nil
locationManager = nil
}
typealias LocationClosure = ((location: CLLocation?, error: NSError?)->())
private var didComplete: LocationClosure?
//location manager returned, call didcomplete closure
private func _didComplete(location: CLLocation?, error: NSError?) {
locationManager?.stopUpdatingLocation()
didComplete?(location: location, error: error)
locationManager?.delegate = nil
locationManager = nil
}
//location authorization status changed
func locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
switch status {
case .AuthorizedWhenInUse:
self.locationManager!.startUpdatingLocation()
case .Denied:
_didComplete(nil, error: NSError(domain: self.classForCoder.description(),
code: OneShotLocationManagerErrors.AuthorizationDenied.rawValue,
userInfo: nil))
default:
break
}
}
internal func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
_didComplete(nil, error: error)
}
internal func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
if let location = locations[0] as? CLLocation {
_didComplete(location, error: nil)
} else {
_didComplete(nil, error: NSError(domain: self.classForCoder.description(),
code: OneShotLocationManagerErrors.InvalidLocation.rawValue,
userInfo: nil))
}
}
//ask for location permissions, fetch 1 location, and return
func fetchWithCompletion(completion: LocationClosure) {
//store the completion closure
didComplete = completion
//fire the location manager
locationManager = CLLocationManager()
locationManager!.delegate = self
//check for description key and ask permissions
if (NSBundle.mainBundle().objectForInfoDictionaryKey("NSLocationWhenInUseUsageDescription") != nil) {
locationManager!.requestWhenInUseAuthorization()
} else if (NSBundle.mainBundle().objectForInfoDictionaryKey("NSLocationAlwaysUsageDescription") != nil) {
locationManager!.requestAlwaysAuthorization()
} else {
fatalError("To use location in iOS8 you need to define either NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription in the app bundle's Info.plist file")
}
}
}

CLLocationManager didUpdateLocations not being called

I am in the process of learning iOS 8 app development with Swift. I have followed a tutorial on Treehouse that walks you through building a weather app in Swift and iOS 8.
As an improvement to the app, the author/tutor suggests using CLLocationManager to get the location of the device to feed into the weather API instead of the hard coded latitude and longitude values.
So having read various tutorial online, I have gone ahead and attempted to implement this suggested improvement.
I have placed the code responsible for getting the location coordinates inside the AppDelegate.swift file.
AppDelegate.swift Code
import UIKit
import CoreLocation
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, CLLocationManagerDelegate {
var window: UIWindow?
var locationManager: CLLocationManager!
var errorOccured: Bool = false
var foundLocation: Bool = false
var locationStatus: NSString = "Not Started"
var location: CLLocationCoordinate2D?
var locationName: String?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
application.setStatusBarHidden(true, withAnimation: .None)
initializeLocationManager()
return true
}
func initializeLocationManager() {
self.locationManager = CLLocationManager()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
self.locationManager.requestAlwaysAuthorization()
self.locationManager.startUpdatingLocation()
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
println("didUpdateLocations running")
if (foundLocation == false) {
self.locationManager.stopUpdatingLocation()
foundLocation = true
var locationArray = locations as NSArray
var locationObj = locationArray.lastObject as CLLocation
var geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(locationObj, completionHandler: { (placemarks, error) -> Void in
var p = placemarks as NSArray
var placemark: CLPlacemark? = p.lastObject as? CLPlacemark
self.locationName = placemark?.name
})
self.location = locationObj.coordinate
}
}
func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
locationManager.stopUpdatingLocation()
if ((error) != nil) {
if (errorOccured == false) {
errorOccured = true
print(error)
}
}
}
// authorization status
func locationManager(manager: CLLocationManager!,
didChangeAuthorizationStatus status: CLAuthorizationStatus) {
var shouldIAllow = false
switch status {
case CLAuthorizationStatus.Restricted:
locationStatus = "Restricted Access to location"
case CLAuthorizationStatus.Denied:
locationStatus = "User denied access to location"
case CLAuthorizationStatus.NotDetermined:
locationStatus = "Status not determined"
default:
locationStatus = "Allowed to location Access"
shouldIAllow = true
}
NSNotificationCenter.defaultCenter().postNotificationName("LabelHasbeenUpdated", object: nil)
if (shouldIAllow == true) {
NSLog("Location to Allowed")
// Start location services
locationManager.startUpdatingLocation()
} else {
NSLog("Denied access: \(locationStatus)")
}
}
}
And then in my ViewController.swift file I want to obtain the location coordinates. Here is the code:
ViewController.swift Code
func getCurrentWeatherData() -> Void {
let baseURL = NSURL(string: "https://api.forecast.io/forecast/\(apiKey)/")
var forecastURL: NSURL
var locName = "London"
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
appDelegate.foundLocation = false
if let loc = appDelegate.location {
println("Got Location!") // for debug purposes
var currentLat = loc.latitude
var currentLng = loc.longitude
forecastURL = NSURL(string: "\(currentLat),\(currentLng)", relativeToURL: baseURL)
locName = appDelegate.locationName!
} else {
println("No Location :(") // for debug purposes
var currentLat = "51.513445"
var currentLng = "-0.157828"
forecastURL = NSURL(string: "\(currentLat),\(currentLng)", relativeToURL: baseURL)
}
let sharedSession = NSURLSession.sharedSession()
let downloadTask: NSURLSessionDownloadTask = sharedSession.downloadTaskWithURL(forecastURL, completionHandler: { (location: NSURL!, response: NSURLResponse!, error: NSError!) -> Void in
var urlContents = NSString.stringWithContentsOfURL(location, encoding: NSUTF8StringEncoding, error: nil)
if (error == nil) {
let dataObject = NSData(contentsOfURL: location)
let weatherDictionary: NSDictionary = NSJSONSerialization.JSONObjectWithData(dataObject, options: nil, error: nil) as NSDictionary
let currentWeather = Current(weatherDictionary: weatherDictionary)
dispatch_async(dispatch_get_main_queue(), {
() -> Void in
self.locationNameLabel.text = "\(locName)"
self.temperatureLabel.text = "\(currentWeather.temperature)"
self.iconView.image = currentWeather.icon!
self.currentTimeLabel.text = "At \(currentWeather.currentTime!) it is"
self.humidityLabel.text = "\(currentWeather.humidity)"
self.percipitationLabel.text = "\(currentWeather.percipProbability)"
self.summaryLabel.text = "\(currentWeather.summary)"
// Stop refresh animation
self.refreshActivityIndicator.stopAnimating()
self.refreshActivityIndicator.hidden = true
self.refreshButton.hidden = false
})
} else {
let networkIssueController = UIAlertController(title: "Error", message: "Unable to load data. Connectivity error!", preferredStyle: .Alert)
let okButton = UIAlertAction(title: "OK", style: .Default, handler: nil)
networkIssueController.addAction(okButton)
let cancelButton = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
networkIssueController.addAction(cancelButton)
self.presentViewController(networkIssueController, animated: true, completion: nil)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.refreshActivityIndicator.stopAnimating()
self.refreshActivityIndicator.hidden = true
self.refreshButton.hidden = false
})
}
})
downloadTask.resume()
}
The above is not working. My didUpdateLocations delegate never gets called. And in the debug console/output I always get No Location :( printed out, suggesting a failure in getting the location, more specifically suggesting that the location property on my AppDelegate is nil.
Things I have done to remedy this:
In the info.plist I have added the two keys NSLocationWhenInUseUsageDescription and NSLocationAlwaysUsageDescription
Ensured that I am connected via WiFi and not Ethernet
And countless other code tweaks, and still nothing.
A couple of observations:
As you point out, if you're going to call requestAlwaysAuthorization, then you must set NSLocationAlwaysUsageDescription. If you called requestWhenInUseAuthorization, you'd need NSLocationWhenInUseUsageDescription. (The fact that you see the confirmation dialog means that you've done this correctly. I assume you are seeing whatever description you supplied in the confirmation alert.)
On your simulator, you may not see location updates like on a device. Test this on an actual device.
When I used your code, I see didUpdateLocations when I called this from a device, but not from the simulator.
Once you solve the issue of not seeing didUpdateLocations being called, there is another issue:
You are posting a notification when the authorization status changes, but not when a location is received asynchronously (i.e. later). Frankly, the latter is the more critical event from the view controller's perspective, so I would have thought that (a) you should post a notification when the location is received; and (b) the view controller should observe this notification. Right now, even if you succeed in getting didUpdateLocations to be called, the view controller won't be notified of such.
Also, your didUpdateLocations is initiating yet another asynchronous process, the geocode of the coordinate. If your view controller needs that, too, you should post a notification inside the completion block of the geocoder.
Frankly, you haven't even shown us the view controller code that adds an observer for whatever notifications that this CLLocationManagerDelegate code will invoke, but I assume you have done that.
Just for the record: I first put the two keys (NSLocationAlwaysUsageDescription and NSLocationWhenInUseUsageDescription) into the test-plist instead of the application-plist..Took me some time to realize.....

Resources