XCode iOS Simulator get location buggy? - ios

Is it just me or is the iOS Simulator slow when trying to obtain location? It sometimes takes multiple calls to obtain the information.
For example, I'll set location to New York, update my labels to show "New York, NY", change location to Hong Kong, and try to update my labels again, but it takes between 2 and 5 method calls before the labels change to HK.
Is my location code inefficient or is the iOS simulator buggy for these requests?
#IBAction func findMyLocation(sender: AnyObject) {
// set location manager, set accuracy and start updating
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
CLGeocoder().reverseGeocodeLocation(manager.location) {
(let placemarks, let error) in
if error != nil {
println("Reverse geocoder failed with error" + error.localizedDescription)
return
}
if placemarks.count > 0 && placemarks != nil {
let placemark = placemarks[0] as! CLPlacemark
let locationsArray = locations as NSArray
self.displayLocationInfo(placemark, locations: locationsArray)
}
}
}
func displayLocationInfo(placemark: CLPlacemark, locations: NSArray) {
// stop updating to save power
locationManager.stopUpdatingLocation()
cityLabel?.text = placemark.locality
zipcodeLabel?.text = placemark.postalCode
stateLabel?.text = placemark.administrativeArea
countryLabel?.text = placemark.country
if let locationObj = locations.lastObject as? CLLocation {
let coordinates = locationObj.coordinate
coordinatesLabel?.text = "\(coordinates.latitude),\(coordinates.longitude)"
}
}
Please post if there are better ways to write my code! I am new to iOS Development!

Related

Distance filter not working as expected in standard location services

I am using standard location services to track location.
locationManager = CLLocationManager()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
locationManager.distanceFilter = DISTANCE_LIMIT //value set to 20
locationManager.allowsBackgroundLocationUpdates = true
locationManager.startUpdatingLocation()
With the filter distance set, I assumed that the location updates would be received only when the user location changes by the specified distance. However, the updates were received for movements that were less than the preset distance value.
So I set up my delegate method as:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard locations.count > 0 else {
return
}
guard let location = locations.last else {
return
}
let previousLocationEncoded = UserDefaults.standard.object(forKey: UserDefaultKey.previousVisitedLocation) as? Data
if previousLocationEncoded == nil {
let encodedLocation = NSKeyedArchiver.archivedData(withRootObject: location)
UserDefaults.standard.set(encodedLocation, forKey: UserDefaultKey.previousVisitedLocation)
// do some task
} else {
let previousLocationDecoded = NSKeyedUnarchiver.unarchiveObject(with: previousLocationEncoded!) as! CLLocation
let distanceBetweenVisits = previousLocationDecoded.distance(from: location)
if distanceBetweenVisits > DISTANCE_LIMIT {
// do some other task
let encodedLocation = NSKeyedArchiver.archivedData(withRootObject: location)
UserDefaults.standard.set(encodedLocation, forKey: UserDefaultKey.previousVisitedLocation)
}
}
}
The objective is to calculate the distance of current location from previously saved location to determine if the location has changed by the distance filter value.
However, sometimes, even if I am not moving, the condition distanceBetweenVisits > DISTANCE_LIMIT becomes true. Is this something dependent on iOS and the GPS? Can such conditions be properly handled?

Find city name and country from latitude and longitude in Swift

I'm working on application in Swift3
and I have letter problem i can't find the answer for it.
How can I know city name and country short names base on latitude and longitude?
import UIKit
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate{
let locationManager = CLLocationManager()
var latitude: Double = 0
var longitude: Double = 0
override func viewDidLoad() {
super.viewDidLoad()
// For use when the app is open & in the background
locationManager.requestAlwaysAuthorization()
// For use when the app is open
//locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self
locationManager.startUpdatingLocation()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
print(location.coordinate)
latitude = location.coordinate.latitude
longitude = location.coordinate.longitude
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if (status == CLAuthorizationStatus.denied){
showLocationDisabledpopUp()
}
}
func showLocationDisabledpopUp() {
let alertController = UIAlertController(title: "Background Location Access Disabled", message: "We need your location", preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
let openAction = UIAlertAction(title: "Open Setting", style: .default) { (action) in
if let url = URL(string: UIApplicationOpenSettingsURLString){
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
alertController.addAction(openAction)
self.present(alertController, animated: true, completion: nil)
}
}
You can use CLGeocoder reverseGeocodeLocation method to fetch a CLPlacemark and get its country and locality properties info. Note that it is an asynchronous method so you will need to add a completion handler to your method when fetching that info:
import UIKit
import MapKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
extension CLLocation {
func fetchCityAndCountry(completion: #escaping (_ city: String?, _ country: String?, _ error: Error?) -> ()) {
CLGeocoder().reverseGeocodeLocation(self) { completion($0?.first?.locality, $0?.first?.country, $1) }
}
}
Usage
let location = CLLocation(latitude: -22.963451, longitude: -43.198242)
location.fetchCityAndCountry { city, country, error in
guard let city = city, let country = country, error == nil else { return }
print(city + ", " + country) // Rio de Janeiro, Brazil
}
edit/update:
iOS 11 or later CLPlacemark has a postalAddress property. You can import Contacts framework and use CNPostalAddressFormatter's string(from:) method to get a localized formatted address. You can also extend CLPlacemark and add some computed properties to better describe some of its properties:
import MapKit
import Contacts
extension CLPlacemark {
/// street name, eg. Infinite Loop
var streetName: String? { thoroughfare }
/// // eg. 1
var streetNumber: String? { subThoroughfare }
/// city, eg. Cupertino
var city: String? { locality }
/// neighborhood, common name, eg. Mission District
var neighborhood: String? { subLocality }
/// state, eg. CA
var state: String? { administrativeArea }
/// county, eg. Santa Clara
var county: String? { subAdministrativeArea }
/// zip code, eg. 95014
var zipCode: String? { postalCode }
/// postal address formatted
#available(iOS 11.0, *)
var postalAddressFormatted: String? {
guard let postalAddress = postalAddress else { return nil }
return CNPostalAddressFormatter().string(from: postalAddress)
}
}
extension CLLocation {
func placemark(completion: #escaping (_ placemark: CLPlacemark?, _ error: Error?) -> ()) {
CLGeocoder().reverseGeocodeLocation(self) { completion($0?.first, $1) }
}
}
Usage:
let location = CLLocation(latitude: 37.331676, longitude: -122.030189)
location.placemark { placemark, error in
guard let placemark = placemark else {
print("Error:", error ?? "nil")
return
}
print(placemark.postalAddressFormatted ?? "")
}
This will print
1 Infinite Loop
Cupertino CA 95014
United States
I would recommend integrating Google Maps API with your project. If you do, your task can be achieved using Reverse Geocoding Google provides.
Furthermore, Google there is Google Maps SDK for IOS development, which is also worth considering.
UPD: You can do that without integrating maps into your project. Basing on this answer, you can achieve that using http requests to Google API. The request to:
https://maps.googleapis.com/maps/api/geocode/json?latlng=40.714224,-73.961452&key=API_KEY
would return JSON object with information about the requested place, including country and city name.
BTW, I highly recommend using Alamofire to make http requests in Swift.
What you need is called reverse geocoding. As you have already declared some properties at the top. You need to add the CLGeocoder & CLPlancemark
let locationManager = CLLocationManager()
var location: CLLocation?
let geocoder = CLGeocoder()
var placemark: CLPlacemark?
// here I am declaring the iVars for city and country to access them later
var city: String?
var country: String?
var countryShortName: String?
Create a function where you can start the location services
func startLocationManager() {
// always good habit to check if locationServicesEnabled
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
}
also create another to stop once you're done with location geocoding
func stopLocationManager() {
locationManager.stopUpdatingLocation()
locationManager.delegate = nil
}
in view didLoad or from anywhere you want to start the location manager add a check first
override func viewDidLoad() {
super.viewDidLoad()
let authStatus = CLLocationManager.authorizationStatus()
if authStatus == .notDetermined {
locationManager.requestWhenInUseAuthorization()
}
if authStatus == .denied || authStatus == .restricted {
// add any alert or inform the user to to enable location services
}
// here you can call the start location function
startLocationManager()
}
implement the delegate methods for location manager didFailedWithError
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
// print the error to see what went wrong
print("didFailwithError\(error)")
// stop location manager if failed
stopLocationManager()
}
implement the delegate method for location manager didUpdateLocations
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// if you need to get latest data you can get locations.last to check it if the device has been moved
let latestLocation = locations.last!
// here check if no need to continue just return still in the same place
if latestLocation.horizontalAccuracy < 0 {
return
}
// if it location is nil or it has been moved
if location == nil || location!.horizontalAccuracy > lastLocation.horizontalAccuracy {
location = lastLocation
// stop location manager
stopLocationManager()
// Here is the place you want to start reverseGeocoding
geocoder.reverseGeocodeLocation(lastLocation, completionHandler: { (placemarks, error) in
// always good to check if no error
// also we have to unwrap the placemark because it's optional
// I have done all in a single if but you check them separately
if error == nil, let placemark = placemarks, !placemark.isEmpty {
self.placemark = placemark.last
}
// a new function where you start to parse placemarks to get the information you need
self.parsePlacemarks()
})
}
}
Add the parsePlacemarks function
parsePlacemarks() {
// here we check if location manager is not nil using a _ wild card
if let _ = location {
// unwrap the placemark
if let placemark = placemark {
// wow now you can get the city name. remember that apple refers to city name as locality not city
// again we have to unwrap the locality remember optionalllls also some times there is no text so we check that it should not be empty
if let city = placemark.locality, !city.isEmpty {
// here you have the city name
// assign city name to our iVar
self.city = city
}
// the same story optionalllls also they are not empty
if let country = placemark.country, !country.isEmpty {
self.country = country
}
// get the country short name which is called isoCountryCode
if let countryShortName = placemark.isoCountryCode, !countryShortName.isEmpty {
self.countryShortName = countryShortName
}
}
} else {
// add some more check's if for some reason location manager is nil
}
}
You have to cmd+click on CLPlacemark to see all the properties that you can access for example street name is called thoroughfare & the number is is called subThoroughfare continue reading the documentation for more information
Note: You have to check for locations error also geocoder error which I haven't implemented here but you have to take care of those errors and the best place to check error codes and everything else is apples documentation
Update: Check paresPlacemarks function where I added isoCountryCode which is equal to country shortName No need to add extra network calls to google API and Alamofire while your already using location services
Here is the Swift 4 code:
var locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
locationManager.startMonitoringSignificantLocationChanges()
// Here you can check whether you have allowed the permission or not.
if CLLocationManager.locationServicesEnabled()
{
switch(CLLocationManager.authorizationStatus())
{
case .authorizedAlways, .authorizedWhenInUse:
print("Authorize.")
let latitude: CLLocationDegrees = (locationManager.location?.coordinate.latitude)!
let longitude: CLLocationDegrees = (locationManager.location?.coordinate.longitude)!
let location = CLLocation(latitude: latitude, longitude: longitude) //changed!!!
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
if error != nil {
return
}else if let country = placemarks?.first?.country,
let city = placemarks?.first?.locality {
print(country)
self.cityNameStr = city
}
else {
}
})
break
case .notDetermined:
print("Not determined.")
self.showAlertMessage(messageTitle: "Bolo Board", withMessage: "Location service is disabled!!")
break
case .restricted:
print("Restricted.")
self.showAlertMessage(messageTitle: "Bolo Board", withMessage: "Location service is disabled!!")
break
case .denied:
print("Denied.")
}
}
}
func showAlertMessage(messageTitle: NSString, withMessage: NSString) ->Void {
let alertController = UIAlertController(title: messageTitle as String, message: withMessage as String, preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (action:UIAlertAction!) in
}
alertController.addAction(cancelAction)
let OKAction = UIAlertAction(title: "Settings", style: .default) { (action:UIAlertAction!) in
if let url = URL(string: "App-Prefs:root=Privacy&path=LOCATION/com.company.AppName") {
if #available(iOS 10.0, *) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
// Fallback on earlier versions
}
}
}
alertController.addAction(OKAction)
self.present(alertController, animated: true, completion:nil)
}
You can use CLGeocoder, from CoreLocation, for that. From Apple documentation (emphasizes mine):
A single-shot object for converting between geographic coordinates and place names.
The CLGeocoder class provides services for converting between a coordinate (specified as a latitude and longitude) and the user-friendly representation of that coordinate. A user-friendly representation of the coordinate typically consists of the street, city, state, and country information corresponding to the given location...
This service is unrelated to MapKit and, as such, don't require you use/show a map in your app at all.
import Foundation
import CoreLocation
let location = CLLocation(latitude: 37.3321, longitude: -122.0318)
CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
guard let placemark = placemarks?.first else {
let errorString = error?.localizedDescription ?? "Unexpected Error"
print("Unable to reverse geocode the given location. Error: \(errorString)")
return
}
let reversedGeoLocation = ReversedGeoLocation(with: placemark)
print(reversedGeoLocation.formattedAddress)
// Apple Inc.,
// 1 Infinite Loop,
// Cupertino, CA 95014
// United States
}
struct ReversedGeoLocation {
let name: String // eg. Apple Inc.
let streetName: String // eg. Infinite Loop
let streetNumber: String // eg. 1
let city: String // eg. Cupertino
let state: String // eg. CA
let zipCode: String // eg. 95014
let country: String // eg. United States
let isoCountryCode: String // eg. US
var formattedAddress: String {
return """
\(name),
\(streetNumber) \(streetName),
\(city), \(state) \(zipCode)
\(country)
"""
}
// Handle optionals as needed
init(with placemark: CLPlacemark) {
self.name = placemark.name ?? ""
self.streetName = placemark.thoroughfare ?? ""
self.streetNumber = placemark.subThoroughfare ?? ""
self.city = placemark.locality ?? ""
self.state = placemark.administrativeArea ?? ""
self.zipCode = placemark.postalCode ?? ""
self.country = placemark.country ?? ""
self.isoCountryCode = placemark.isoCountryCode ?? ""
}
}
1 . import CoreLocation
2 . insert CLLocationManagerDelegate in your class
3 . Do the delegate methods described below... hope it will help you
you can find city name and country through following these steps...Here is my code
import UIKit
import CoreLocation
class MyViewController:UIViewController,CLLocationManagerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.requestAlwaysAuthorization()
self.locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if( CLLocationManager.authorizationStatus() == .authorizedWhenInUse ||
CLLocationManager.authorizationStatus() == .authorizedAlways){
if let currentLocation = locationManager.location
{
if NetworkFunctions.NetworkRechability()
{
getAddressFromLatLon(pdblLatitude: "\(Double((currentLocation.coordinate.latitude)))", withLongitude: "\(Double((currentLocation.coordinate.longitude)))")
}
}
}
}
func getAddressFromLatLon(pdblLatitude: String, withLongitude pdblLongitude: String) {
var center : CLLocationCoordinate2D = CLLocationCoordinate2D()
let lat: Double = Double("\(pdblLatitude)")!
let lon: Double = Double("\(pdblLongitude)")!
let ceo: CLGeocoder = CLGeocoder()
center.latitude = lat
center.longitude = lon
let loc: CLLocation = CLLocation(latitude:center.latitude, longitude: center.longitude)
ceo.reverseGeocodeLocation(loc, completionHandler:
{(placemarks, error) in
if (error != nil)
{
}
if placemarks != nil
{
let pm = placemarks! as [CLPlacemark]
if pm.count > 0 {
let pm = placemarks![0]
print(pm.country ?? "")
print(pm.locality ?? "")
print(pm.subLocality ?? "")
print(pm.thoroughfare ?? "")
print(pm.postalCode ?? "")
print(pm.subThoroughfare ?? "")
var addressString : String = ""
if pm.subLocality != nil {
addressString = addressString + pm.subLocality! + ", "
}
if pm.thoroughfare != nil {
addressString = addressString + pm.thoroughfare! + ", "
}
if pm.locality != nil {
addressString = addressString + pm.locality! + ", "
if pm.country != nil {
addressString = addressString + pm.country! + ", "
//uuuuu
if(location_city != pm.locality!.trimmingCharacters(in: .whitespaces))
{
location_city=pm.locality!.trimmingCharacters(in: .whitespaces)
DispatchQueue.main.async{
self.GetBeeWatherDetails(district: pm.locality!, country: pm.country!)
}
}
}
}
if pm.postalCode != nil {
addressString = addressString + pm.postalCode! + " "
}
}
}
})
}
}
Add this extension in your swift file.
extension CLLocation {
func fetchAddress(completion: #escaping (_ address: String?, _ error: Error?) -> ()) {
CLGeocoder().reverseGeocodeLocation(self) {
let palcemark = $0?.first
var address = ""
if let subThoroughfare = palcemark?.subThoroughfare {
address = address + subThoroughfare + ","
}
if let thoroughfare = palcemark?.thoroughfare {
address = address + thoroughfare + ","
}
if let locality = palcemark?.locality {
address = address + locality + ","
}
if let subLocality = palcemark?.subLocality {
address = address + subLocality + ","
}
if let administrativeArea = palcemark?.administrativeArea {
address = address + administrativeArea + ","
}
if let postalCode = palcemark?.postalCode {
address = address + postalCode + ","
}
if let country = palcemark?.country {
address = address + country + ","
}
if address.last == "," {
address = String(address.dropLast())
}
completion(address,$1)
// completion("\($0?.first?.subThoroughfare ?? ""), \($0?.first?.thoroughfare ?? ""), \($0?.first?.locality ?? ""), \($0?.first?.subLocality ?? ""), \($0?.first?.administrativeArea ?? ""), \($0?.first?.postalCode ?? ""), \($0?.first?.country ?? "")",$1)
}
}
}
And then call it on any of the CLLocation object.
Eg:
(myLocation as? CLLocation)!.fetchAddress { (address, error) in
guard let address = address, error == nil else
{return }
I had also the same issue .You can use this code.
func placePicker(_ viewController: GMSPlacePickerViewController, didPick place: GMSPlace) {
viewController.dismiss(animated: true, completion: nil)
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: place.coordinate.latitude, longitude: place.coordinate.longitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in
// Place details
var placeMark: CLPlacemark!
placeMark = placemarks?[0]
// Address dictionary
print(placeMark.addressDictionary as Any)
//
print("Place name \(place.name)")
print("Place address \(String(describing: place.formattedAddress))")
print("Place attributions \(String(describing: place.attributions))")
})
}
Hope this will resolve your problem.
This method will give you the current location, city name ,country name etc.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location: CLLocation = locations.last!
print("Location: \(location)")
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
// Process Response
if let error = error {
print("Unable to Reverse Geocode Location (\(error))")
} else {
if let placemarks = placemarks, let placemark = placemarks.first {
self.city = placemark.locality!
//self.country = placemark.country!
}
}
}
let camera = GMSCameraPosition.camera(withLatitude: location.coordinate.latitude,
longitude: location.coordinate.longitude,
zoom: zoomLevel)
self.locationv = CLLocation(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
if myView.isHidden {
myView.isHidden = false
myView.camera = camera
} else {
myView.animate(to: camera)
}
}
See my answer in swift 4.1 Xcode 9.4.1. You can get even village name details also. Get location name from Latitude & Longitude in iOS

Is there a way to pre-populate the GMSAutocompleteViewController with a data array of current nearby locations?

I've tried to generate a likelihood list of places with the currentPlaceWithCallback, which works fine. However, I'm having difficult figuring out a way to insert the information gathered from this likelihood list into the view of the GMSAutocompleteViewController / GMSAutocompleteResultsViewController before the user starts typing a search (this functionality works fine).
Any suggestions would be greatly appreciated!
func generateLikelihoodListViaPlacesClient() {
placesClient.currentPlaceWithCallback { (likelihoodlist, error) -> Void in
if error != nil {
println("Current Place error: \(error!.localizedDescription)")
return
}
for likelihood in likelihoodlist!.likelihoods {
let nearestPlace = likelihoodlist!.likelihoods.first
println(nearestPlace)
if let likelihood = likelihood as? GMSPlaceLikelihood {
let place = likelihood.place
self.placesArray!.insert(place.name, atIndex: 0)
println("Current Place name \(place.name) at likelihood \(likelihood.likelihood)")
println("Current Place address \(place.formattedAddress)")
println("Current Place attributions \(place.attributions)")
println("Current PlaceID \(place.placeID)")
}
}
}
}
My current GMSAutocompleteViewController is presented after clicking on a UITextField and triggering the EditingDidBegin textfield delegate function. I've set the bounds by using my location manager which has already been previously used:
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let newLocation = locations.last {
println("#######################")
println("\(newLocation.coordinate)")
println("#######################")
// Creates northeast and southwest coordinate bounds for rectangle that autocompleteController returns results in
let currentCoordinate2D : CLLocationCoordinate2D = newLocation.coordinate
let northEastCoordinate2DWithBearing : CLLocationCoordinate2D = locationWithBearing(270, distanceMeters: 500, origin: currentCoordinate2D)
let southWestCoordinate2DWithBearing : CLLocationCoordinate2D = locationWithBearing(90, distanceMeters: 500, origin: currentCoordinate2D)
let bounds : GMSCoordinateBounds = GMSCoordinateBounds(coordinate: northEastCoordinate2DWithBearing, coordinate: southWestCoordinate2DWithBearing)
autocompleteController.autocompleteBounds = bounds
// Restricts filter to only businesses
let filter : GMSAutocompleteFilter = GMSAutocompleteFilter()
filter.type = .Establishment
autocompleteController.autocompleteFilter = filter
if #available(iOS 9.0, *) {
} else {
// Fallback on earlier versions
locationManager.stopUpdatingLocation()
}
}
}

Calling asynchronous tasks one after the other in Swift (iOS)

I'm having problems with trying to run several asynchronous tasks one by one using dispatch async in swift, for an iOS weather app. I want my 'update()' function to:
get the user's location (and store the latitude & longitude in the class variables)
when location services is complete, make a call to the weather API based on newly-populated lat & long variables
when API call (and subsequent XML parsing) is complete, update the UI (an iOS table view)
(Go easy on me, I'm a recently self-taught coder, so I'm assuming the more experienced of you out there will be able to point out various errors! Any help would be massively appreciated.)
var latitude: String = ""
var longitude: String = ""
var locationManager: CLLocationManager!
var forecastData: Weather = Weather() // the weather class has it's own asynchronous NSURLSession called retrieveForecast()
// which calls the Open Weather Map API and parses the XML
func refresh() {
// Here's where I don't know what I'm doing:
let group = dispatch_group_create()
dispatch_group_enter(group)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
self.getLocation()
dispatch_group_leave(group)
}
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
self.getWeather()
}
self.updateUI() // ...and I know that this is in totally the wrong place!
}
// function(s) to get phone's location:
func getLocation() {
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.requestWhenInUseAuthorization()
locationManager.distanceFilter = 100.0
locationManager.startUpdatingLocation()
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
self.locationManager.stopUpdatingLocation()
manager.stopUpdatingLocation()
for item: AnyObject in locations {
if let location = item as? CLLocation {
if location.horizontalAccuracy < 1000 {
manager.stopUpdatingLocation()
self.latitude = String(location.coordinate.latitude)
self.longitude = String(location.coordinate.longitude)
}
}
}
}
// function to download and parse the weather data within forecastData object
func getWeather() {
let apiCall = "http://api.openweathermap.org/data/2.5/forecast?lat=" + self.latitude
+ "&lon=" + self.longitude + "&mode=xml&appid=" + self.openWeatherMapAPIKey
NSLog("getWeather called with api request: \(apiCall)")
self.forecastData.retrieveForecast(apiCall)
}
For any asynchronous operation it is a good manner to have a finish callback.
In your case, if you have implemented callbacks for getLocation and getWeather you'll never need dispatch_groups, you just call getWeather from getLocation's callback, and then you just call refreshUI from getWeather's callback.
var latitude: String = ""
var longitude: String = ""
var locationManager: CLLocationManager!
var forecastData: Weather = Weather() // the weather class has it's own asynchronous NSURLSession called retrieveForecast()
// which calls the Open Weather Map API and parses the XML
func refresh() {
self.getLocation()
}
// function(s) to get phone's location:
func getLocation() {
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.requestWhenInUseAuthorization()
locationManager.distanceFilter = 100.0
locationManager.startUpdatingLocation()
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
self.locationManager.stopUpdatingLocation()
manager.stopUpdatingLocation()
for item: AnyObject in locations {
if let location = item as? CLLocation {
if location.horizontalAccuracy < 1000 {
manager.stopUpdatingLocation()
self.latitude = String(location.coordinate.latitude)
self.longitude = String(location.coordinate.longitude)
self.getWeather()
}
}
}
}
// function to download and parse the weather data within forecastData object
func getWeather() {
let apiCall = "http://api.openweathermap.org/data/2.5/forecast?lat=" + self.latitude
+ "&lon=" + self.longitude + "&mode=xml&appid=" + self.openWeatherMapAPIKey
NSLog("getWeather called with api request: \(apiCall)")
self.forecastData.retrieveForecast(apiCall)
// assuming that self.forecastData.retrieveForecast(apiCall) is completely synchronous, we can call updateUI below
self.updateUI()
}
Here is the code, demonstrating how dispatch_group can be used right way:
func refetchOrdersAndChats(remoteNotificationData: [NSObject : AnyObject], completion: ((Bool)->())?) {
var overallSuccess: Bool = true
let refetchGroup = dispatch_group_create();
dispatch_group_enter(refetchGroup);
CPChatController.sharedInstance.updateChat(remoteNotificationData, completion: { success in
overallSuccess = success && overallSuccess
dispatch_group_leave(refetchGroup);
})
dispatch_group_enter(refetchGroup);
CPChatController.sharedInstance.fetchNewOrdersWithNotification(remoteNotificationData, completion: { success in
overallSuccess = success && overallSuccess
dispatch_group_leave(refetchGroup);
})
dispatch_group_notify(refetchGroup,dispatch_get_main_queue(), {
completion?(overallSuccess)
})
}
After going off to learn about closures/blocks, I finally found my answer so thought I'd share. What I needed to do was add a closure to the arguments in my Weather class, which returns a boolean value when the XML parsing is complete, and allows me to wait for that to happen in my View Controller before updating the UI. I hope this helps anyone else looking for a similar answer, and if any pros are able to make this code even better, please do add a comment!
...
// getWeather function in my View Controller (called from location manager)
func getWeather() {
let apiCall = "http://api.openweathermap.org/data/2.5/forecast?lat=" + self.latitude
+ "&lon=" + self.longitude + "&mode=xml&appid=" + self.openWeatherMapAPIKey
NSLog("getWeather called with api request: \(apiCall)")
self.forecastData.retrieveForecast(apiCall, completion: { success in
if success {
self.updateUI()
} else {
NSLog("Parse unsuccessful")
// I'll handle the issue here
}
});
}
...And in the Weather class function:
func retrieveForecast(url: String, completion: ((Bool)->())?) {
self.reset()
let apiCall = NSURL(string: url)
let urlRequest: NSURLRequest = NSURLRequest(URL: apiCall!)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(urlRequest) {
(data, response, error) -> Void in
self.xmlParser = NSXMLParser(data: data!)
self.xmlParser.delegate = self
let success: Bool = self.xmlParser.parse()
if !success {
NSLog("Did not parse. \(error?.localizedDescription)")
completion?(false)
} else {
self.currentParsedElement = ""
self.retrievingForecast = false
completion?(true)
}
}
task.resume()
}

Is it possible to have custom reverse geocoding?

Currently, I'm utilizing reverse geocoding to simply convert a longitude and latitude to a locality and sub locality.
Is it possible for me to override this and essentially have it return custom strings at my discretion given that I provide it with the coordinates? Any thoughts?
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
locationManager.stopUpdatingLocation()
if(locations.count > 0){
let location = locations[0] as! CLLocation
// println(location.coordinate)
if let currentLocatino = currLocation {
if CLLocation(latitude: currentLocatino.latitude, longitude: currentLocatino.longitude).distanceFromLocation(location) > 500 {
currLocation = location.coordinate
self.skip = 0
self.loadObjects()
}
}
else {
currLocation = location.coordinate
self.skip = 0
self.loadObjects()
}
CLGeocoder().reverseGeocodeLocation(CLLocation(latitude: currLocation!.latitude, longitude: currLocation!.longitude), completionHandler: {(placemarks, error) -> Void in
if error != nil {
println("Reverse geocoder failed with error" + error.localizedDescription)
return
}
if placemarks.count > 0 {
let date = NSDate()
let formatter = NSDateFormatter()
formatter.dateStyle = .MediumStyle
formatter.stringFromDate(date)
let pm = placemarks[0] as! CLPlacemark
var testifempty = "\(pm.subLocality)"
if testifempty == "nil"
{
self.locationManager.startUpdatingLocation()
if let lbutton = self.lbutton{
lbutton.text = "Hello " + "\(pm.locality)" //+ "\n" + formatter.stringFromDate(date)
}
}
else
{
self.locationManager.startUpdatingLocation()
if let lbutton = self.lbutton {
lbutton.text = "Hello " + "\(pm.subLocality)\n" // + formatter.stringFromDate(date)
}
}
}
else {
println("Problem with the data received from geocoder")
}
})
} else {
println("Cannot fetch your location")
}
}
If you know what the names are, you might try a switch that swapped out matching strings.
var locale_string
switch locale_string {
case “name1”:
let displayed_locale_name = “changed_name1”
case “name2”:
let displayed_locale_name = “changed_name2”
...
}
Then, the default case where you haven't identified the locale could accept the string as it is.
I got the idea from the Swift2 Programming Language Guide, page 17.
Reverse geocoding is basically a spatial query. I would suggest to define a map with spatial boundaries, assign each boundary an attribute for both locality and sub-locality.
With that map, you can run your set of coordinates against it and assign them the same attributes as the coordinate fits in one of the spatial boundaries.

Resources