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.....
Related
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))")
})
}
})
}
I created a custom Sirikit intent, in IntentHandler class I can not find the location of user where location privacy is set by default 'Always'.
please look at the code.
import Foundation
import CoreData
import CoreLocation
class PhotoOfTheDayIntentHandler: NSObject, PhotoOfTheDayIntentHandling {
let context = CoreDataStorage.mainQueueContext()
var counter : DistanceEntity?
var locationManger = CLLocationManager()
func confirm(intent: PhotoOfTheDayIntent, completion: #escaping (PhotoOfTheDayIntentResponse) -> Void) {
completion(PhotoOfTheDayIntentResponse(code: .ready, userActivity: nil))
}
func handle(intent: PhotoOfTheDayIntent, completion: #escaping (PhotoOfTheDayIntentResponse) -> Void) {
self.context.performAndWait{ () -> Void in
let counter = NSManagedObject.findAllForEntity("DistanceEntity", context: self.context)
if (counter?.last != nil) {
self.counter = (counter!.last as! DistanceEntity)
let currentLocation: CLLocation = locationManger.location!
let greenLocation = CLLocation(latitude:self.counter!.latitude, longitude: self.counter!.longitude)
let distanceInMeters = currentLocation.distance(from: greenLocation) // result is in meters
debugPrint("distanceInMeters",distanceInMeters)
completion(PhotoOfTheDayIntentResponse.success(photoTitle: "\(distanceInMeters) Meter"))
completion(PhotoOfTheDayIntentResponse.success(photoTitle: "\(self.counter!.distance) Meter"))
}
}
}
}
if I comment the location manager it crash.
TLDR: Create CLLocationManager in the main thread and it should work
If you open the Console.app on your mac and monitoring the device you are running the Siri Intent on you will probably se a message similar to this:
A location manager (0xe86bdf0) was created on a dispatch queue executing on a thread other than the main thread.
(Just like in this question: location manager was created on a dispatch queue.)
The problem is that core location must be created in a run loop that is attached to the main loop. Easiest solution is to create the CLLocationManager in the main loop.
Here is an example intent handler that uses location.
import Foundation
import CoreLocation
class ExampleIntentHandler: NSObject, ExampleIntentIntentHandling, CLLocationManagerDelegate {
private var locationManager: CLLocationManager?
var onDidChangeAuthorization: ((ExampleIntentResponse) -> Void)?
var onDidUpdateLocations: ((ExampleIntentResponse) -> Void)?
func confirm(intent: CurrentSpeedIntent, completion: #escaping (CurrentSpeedIntentResponse) -> Void) {
DispatchQueue.main.async {
self.onDidChangeAuthorization = completion
self.locationManager = CLLocationManager()
self.locationManager?.delegate = self
self.locationManager?.requestWhenInUseAuthorization()
}
}
func handle(intent: CurrentSpeedIntent, completion: #escaping (CurrentSpeedIntentResponse) -> Void) {
DispatchQueue.main.async {
self.onDidUpdateLocations = completion
self.locationManager = CLLocationManager()
self.locationManager?.delegate = self
self.locationManager?.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager?.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .authorizedAlways, .authorizedWhenInUse:
let response = ExampleIntentResponse(code: .ready, userActivity: nil)
onDidChangeAuthorization?(response)
default:
let response = ExampleIntentResponse(code: .failure, userActivity: nil)
onDidChangeAuthorization?(response)
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else {
return
}
// Do something with the `location` but note that this
// method could be called multiple times by iOS. So if
// you do more that just responding, like fetching a
// photo, or manipulate something in your database you
// will probably set some kind of variable here and
// stop if that is already set.
//
// Example:
// guard intentHandled == false else { return }
// intentHandled = true
//
// The `intentHandled` must of course be a instance variable
// Don't forget to respond!
let response = ExampleIntentResponse(code: .success, userActivity: nil)
self.onDidUpdateLocations?(response)
}
}
This will also execute only when there actually is a location. I can see that you are force unwrapping your location which is a bad practice because it may be nil and then your intent will just crash. Here we will instead do what we need when we have the location.
Request to use location must also be made in the app first if that is not done yet.
This is a really basic outlay of what I am using...
import UIKit
import MapKit
import CoreLocation
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet weak var mapView: MKMapView!
// Call the locationManager class
let LocationManager = CLLocationManager()
// CoreData Delegate
let appDelegate = UIApplication.shared.delegate as! AppDelegate
override func viewDidLoad() {
super.viewDidLoad()
// Conform to Delegate Method
self.LocationManager.delegate = self
// Set required accuracy
self.LocationManager.desiredAccuracy = kCLLocationAccuracyBest
// Blue dot
self.mapView.showsUserLocation = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// check location services active
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
// check location services
switch CLLocationManager.authorizationStatus() {
case .authorizedAlways:
self.LocationManager.startUpdatingLocation()
case .notDetermined:
self.LocationManager.requestAlwaysAuthorization()
case .authorizedWhenInUse, .restricted, .denied:
let alertController = UIAlertController(
title: "Background Location Access Disabled",
message: "In order to work your location settings need to be set to '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.shared.open(url as URL)
}
}
alertController.addAction(openAction)
self.present(alertController, animated: true, completion: nil)
}
}
// Location delegate methods
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print(locations)
// get last location
let location = locations.last
print(location!.coordinate.latitude)
// set region
let region = MKCoordinateRegion(center: location!.coordinate, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
// deploy region to map
self.mapView.setRegion(region, animated: true)
// Map to follow the user
self.mapView.setUserTrackingMode(MKUserTrackingMode.follow, animated: true)
// Show compass on map
self.mapView.showsCompass = true
// save the location data to CoreData
//self.save(latitude: location!.coordinate.latitude, longitude: location!.coordinate.longitude)
// end Location updating
self.LocationManager.stopUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Errors: " + error.localizedDescription)
}
}
My issue is that func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] calls itself over and over again (around 3 times on initial load)...
I am using the .last which AFAIK is meant to pull out the last result in that object.. which it probably is, as with breakpoints inserted it after the first 2 prints it only returns 1 lot of results...
After searching high and low, I am hoping I can get a result by asking the question... Thanks!
Console output of my issue:
When you call startUpdatingLocation() the location manager immediately starts the delivering of location data. The first incoming locations may be way off your actual location, so check the horizontalAccuracy and verticalAccuracy attributes and dismiss locations which are too inaccurate.
It looks like you just want to get a one-shot location, if so try this code:
// Use:
// at class level:
// var manager: LocationOneShotManager?
// in viewDidLoad:
// manager = LocationOneShotManager()
// manager!.fetchWithCompletion {location, error in
// // fetch location or an error
// if let loc = location {
// println(location)
// } else if let err = error {
// println(err.localizedDescription)
// }
// self.manager = nil
// }
import UIKit
import CoreLocation
// possible errors
enum OneShotLocationManagerErrors: Int {
case AuthorizationDenied
case AuthorizationNotDetermined
case InvalidLocation
}
class LocationOneShotManager: 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: [CLLocation]) {
let location = locations[0]
_didComplete(location, error: 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")
}
}
}
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.
Coordinates return nil, I have entered requestAlwaysAuthorization and requestWhenInUseAuthorization in the plist file, but yet nothing happens.
Here is my code.
what is going in this code? I can't find where the error is. It's just returning nil for cordinates
import UIKit
import CoreLocation
class ViewController: UIViewController,CLLocationManagerDelegate {
//Location
var seenError : Bool = false
var locationFixAchieved : Bool = false
var locationStatus : NSString = "Not Started"
var locationManager: CLLocationManager!
var userLocation : String!
var userLatitude : Double!
var userLongitude : Double!
//Current weather Outlets "Display Objects"
#IBOutlet weak var userLocationLabel: UILabel!
#IBOutlet weak var visibilityLabel: UILabel!
#IBOutlet weak var temperatureLabel: UILabel!
#IBOutlet weak var iconView: UIImageView!
#IBOutlet weak var currentTimeLabel: UILabel!
#IBOutlet weak var humidityLabel: UILabel!
#IBOutlet weak var precipitationLabel: UILabel!
#IBOutlet weak var summaryLabel: UILabel!
#IBOutlet weak var windSpeedLabel: UILabel!
#IBOutlet weak var refreshButton: UIButton!
#IBOutlet weak var refreshActivityIndicator: UIActivityIndicatorView!
//API KEY
private let apiKey = "09ca8e3e75fafbadaf4b8594dabe860e"
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
refreshActivityIndicator.hidden = true
getCurrentWeatherData()
}
//Location Code
func initLocationManager() {
seenError = false
locationFixAchieved = false
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation() }
func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
locationManager.stopUpdatingLocation()
if ((error) != nil) {
if (seenError == false) {
seenError = true
print(error)
}
}
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
CLGeocoder().reverseGeocodeLocation(manager.location, completionHandler: {(placemarks, error)->Void in
let pm = placemarks[0] as! CLPlacemark
self.displayLocationInfo(pm)
})
if (locationFixAchieved == false) {
locationFixAchieved = true
var locationArray = locations as NSArray
var locationObj = locationArray.lastObject as! CLLocation
var coord = locationObj.coordinate
self.userLatitude = coord.latitude
self.userLongitude = coord.longitude
getCurrentWeatherData()
}
}
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 : ""
//println(locality)
//println(postalCode)
//println(administrativeArea)
//println(country)
self.userLocationLabel.text = "\(locality), \(administrativeArea)"
}
}
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)")
}
}
func getCurrentWeatherData() -> Void {
userLocation = "\(userLatitude),\(userLongitude)"
// DC cord. = "44.029002,-92.855343"
//URL
let baseURL = NSURL(string: "https://api.forecast.io/forecast/\(apiKey)/")
let forecastURL = NSURL(string:"\(userLocation)", relativeToURL: baseURL)
// creates data object; to get and save data from online
let sharedSession = NSURLSession.sharedSession()
let downloadTask: NSURLSessionDownloadTask = sharedSession.downloadTaskWithURL(forecastURL!, completionHandler: { (location:NSURL!, response:NSURLResponse!, error: NSError!) -> Void in
// if everything goes alright (no errors then run code)
if (error == nil) {
//turn JSON dictionary to NSDictionary so it's easier to work with
let dataObject = NSData(contentsOfURL: location)
let weatherDictionary: NSDictionary = NSJSONSerialization.JSONObjectWithData(dataObject!, options: nil, error: nil) as! NSDictionary
//bring weather dictionary from the CurrentWeather file
let currentWeather = Current(weatherDictionary: weatherDictionary)
println(weatherDictionary)
//update view with current weather information
dispatch_sync(dispatch_get_main_queue(), { () -> Void in
self.temperatureLabel.text = "\(currentWeather.temperature)"
self.iconView.image = currentWeather.icon!
self.currentTimeLabel.text = "\(currentWeather.currentTime!)"
self.humidityLabel.text = "\(currentWeather.humidity * 100)%"
self.precipitationLabel.text = "\(currentWeather.precipProbability * 100)%"
self.summaryLabel.text = "\(currentWeather.summary)"
self.windSpeedLabel.text = "\(currentWeather.windSpeed) MPH"
self.visibilityLabel.text = "\(currentWeather.visibility) Mi"
//stop refresh animation
self.refreshActivityIndicator.stopAnimating()
self.refreshActivityIndicator.hidden = true
self.refreshButton.hidden = false
})
}
else {
let networkIssueController = UIAlertController(title: "Error", message: "Network Connectivity Error! Can't load data", preferredStyle: .Alert)
println(error)
//information for ok button in alertView
let okButton = UIAlertAction(title: "OK", style: .Default, handler: nil)
networkIssueController.addAction(okButton)
//information for cancel button in alertView
let cancelButton = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
networkIssueController.addAction(cancelButton)
//if an error occurs show alert
self.presentViewController(networkIssueController, animated: true, completion: nil)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
//Stop refresh animation
self.refreshActivityIndicator.stopAnimating()
self.refreshActivityIndicator.hidden = true
self.refreshButton.hidden = false
})
}
})
downloadTask.resume()
}
//refresh method
#IBAction func refresh() {
getCurrentWeatherData()
refreshButton.hidden = true
refreshActivityIndicator.hidden = false
refreshActivityIndicator.startAnimating()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Perhaps you already handled all these situations in code that is not shown, but here is a list of things that I think are missing in your code:
I don't think you call initLocationManager in your code.
getCurrentWeatherData() in your viewDidLoad() won't work, you need to call it from locationManager(didUpdateLocations).
Adding requestAlwaysAuthorization or requestWhenInUseAuthorization alone to the application options is not enough. You also need to provide NSLocationAlwaysUsageDescription with text explaining why you need access.
You need to check the authorization status. If it is undetermined, you need to request access and implement a callback to wait for user response. If you call requestAuthorization with any other status than undetermined, I believe the function does not do anything and does not provide any feedback.
For further troubleshooting I would suggest that you (1) get the status of the authorization and (2) put a print statement or a breakpoint to the locationManager(didUpdateLocations) to see if it gets called at all.
I struggled with this step as well. And MirekE has pointed out all great points. I have built on what MirekE stated and showed yo some code to maybe help explain it in more detail.
Step 1) Make sure your info.plist have the following.Do not forget to give them a value like - Need to use your location to get the weather for your current location. Or some thing along those lines.
NSLocationAlwaysUsageDescription
Privacy - Location Usage Description
NSLocationWhenInUseUsageDescription
Step 2) I added this code to my project and combined with step 1 then I would get the alert views to allow location services.
//this is part of my viewDidLoad()
if ( ios8() ) {
locationManager.requestAlwaysAuthorization()
locationManager.requestWhenInUseAuthorization()
}
locationManager.startUpdatingLocation()
/*
iOS 8 Utility
*/
func ios8() -> Bool {
if ( NSFoundationVersionNumber <= NSFoundationVersionNumber_iOS_7_1 ) {
return false
} else {
return true
}
}
Step 3) I used defaults to store them for another purpose.
// MARK: CLLocationManagerDelegate
public func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
var location:CLLocation = locations[locations.count-1]as! CLLocation
if (location.horizontalAccuracy > 0) {
self.locationManager.stopUpdatingLocation()
self.defaults.setDouble(location.coordinate.latitude, forKey: "lat")
self.defaults.setDouble(location.coordinate.longitude, forKey: "lon")
println("lat \(location.coordinate.latitude) lon \(location.coordinate.longitude)")
defaults.synchronize()
updateLocationInfo() // This is where you would call your getCurrentWeatherData()
}
}
// MARK: locationManager
public func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
println(error)
self.loading.text = "Can't get your location!"
}