How can a change data based off of user's location? - ios

I created a tableview with 2 cells, with each cell showing both your senators (based off of your location). I used CLGeocoder to successfully grab the user's zipcode, and I then put that value (which is of type string) into a variable that declared outside of the function.
Ideally, I want to go to a different function in the class, and use that string variable (which should hold the user's zip code) to create specific data. However, it doesn't work!
Here is the code that extracts the zip code and puts it in var zipCode:(note that the print function in the if condition successfully prints the zip code in the terminal when I run the program).
let locationManager = CLLocationManager()
var zipcode: String = ""
func getTableInfo() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
locationManager.requestWhenInUseAuthorization()
locationManager.startMonitoringSignificantLocationChanges()
CLGeocoder().reverseGeocodeLocation(locationManager.location!, completionHandler: {(placemarks, error) -> Void in
if error != nil {
print("Reverse geocoder failed with error" + error!.localizedDescription)
}
if placemarks!.count > 0 {
let pm = placemarks![0]
self.zipcode = pm.postalCode!
print(self.zipcode)
}
else {
print("Problem with the data received from geocoder")
}
})
}
I call this function in viewDidLoad() and then in the viewDidLoad() function, using an if-statement, I try to use the zip code to change an array of strings. names[] is declared as a empty array of strings right above the viewDidLoad() function.
if zipcode == "94108" {
names[1] = "WORKS!"
print(names)
}
For some reason, it doesn't print the names! (Note that the zip code is indeed 94108 because 94108 is what prints in the console when I ask to print 'zipcode')

Create a completion handler for your getTableInfo method, like this:
typealias ZipcodeCompletionBlock = (String?) -> Void
func getTableInfo(completionBlock: #escaping ZipcodeCompletionBlock) {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
locationManager.requestWhenInUseAuthorization()
locationManager.startMonitoringSignificantLocationChanges()
CLGeocoder().reverseGeocodeLocation(locationManager.location!, completionHandler: {(placemarks, error) -> Void in
if error != nil {
print("Reverse geocoder failed with error" + error!.localizedDescription)
completionBlock(nil)
} else if placemarks!.count > 0 {
let pm = placemarks![0]
self.zipcode = pm.postalCode!
completionBlock(self.zipcode)
} else {
print("Problem with the data received from geocoder")
completionBlock(nil)
}
})
}
Now you can call this function like this inside viewDidLoad:
self.getTableInfo { zipcode in
if zipcode == "94108" {
self.names[1] = "WORKS!"
print(self.names)
}
}

It's probably because of a slight delay in the execution of the code that saves the ZIP code, so zipcode is still "" when your print code is run. Completion handlers are in place because they execute code AFTER the function is done. Put the following code right after print(self.zipcode)
if zipcode == "94108" {
names[1] = "WORKS!"
print(names)
}
Hope that helps!

Related

Code after guard is called later than expected

Hi I am new in iOS development and I am having hard time to understand the following issue. Basically I am trying to get user's name by passing current user's id to Cloud Firestore. However I am having hard time to understand a bug in the code. I can successfully pass the name of user to name variable, while the function returns default value of name which is "" (empty string). It seems that the block of code inside
if let data = snapshot?.data() {
guard let userName = data["name"] as? String else { return }
name = userName
print("after guard") // this line
}
happens later than
print("name") // this line
return name
Full code:
private func returnCurrentUserName() -> String {
// User is signed in.
var name = ""
if let user = Auth.auth().currentUser {
let db = Firestore.firestore()
db.collection("users").document(user.uid).getDocument { (snapshot, error) in
if error == nil {
if let data = snapshot?.data() {
guard let userName = data["name"] as? String else { return }
name = userName
print("after guard") // this line
}
}
}
print("name") // this line
return name
}else {
return ""
}
}
(Note: the query from Cloud Firestore is successful and I can get users name on the console but "name" is printed after "after guard".)
In addition to the other answer:
If you would like to execute code after your operation is done, you could use a completion block (that's just a closure which gets called upon completion):
private func returnCurrentUserName(completion: #escaping () -> ()) -> String {
// User is signed in.
var name = ""
if let user = Auth.auth().currentUser {
let db = Firestore.firestore()
db.collection("users").document(user.uid).getDocument { (snapshot, error) in
if error == nil {
if let data = snapshot?.data() {
guard let userName = data["name"] as? String else { return }
name = userName
completion()//Here you call the closure
print("after guard") // this line
}
}
}
print("name") // this line
return name
}else {
return ""
}
}
How you would call returnCurrentUserName:
returnCurrentUserName {
print("runs after the operation is done")
}
Simplified example:
func returnCurrentUserName(completion: #escaping () -> ()) -> String {
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
completion() //runs after 4 seconds
}
return "xyz"
}
let test = returnCurrentUserName {
print("runs after the operation is done")
}
print(test)
The reason is your getDocument is an asynchronous operation. It takes a callback, and that callback will be invoked when the operation is done. Because of the asynchronous operation, the program will continue process the next line without waiting for the async operation to be completed. That's why you see your print("name") getting executed before the print("after guard")

ReverseGeocodeLocation completions usage in viewDidLoad() swift4.1

Swift version: 4.1
Hello I am a bit more than beginner in swift. Working in an "order application by user locations". Which I control user "country" and "city" name by reversegeocodelocation function before user give order. And write those values in firebase realtime database childs.
my data structure is like
-TR
-Ankara
-userID
-Order(consist of user lat, user long, user mail, userOrder)
It is okay I did that users can order and cancel his/her orders. But also I want to check if users close their phone and return the app, the app should check the database and if there is order given by current user uID it must change the button label, buttonToCancelState = true, and image of our mascot.
This is how I get user coord for order and "countrycode" and "city" for data structure name.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations [0]
if let coord = manager.location?.coordinate {
userLocation = coord
}
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(location) {(placemark, error) in
if error != nil {
print("there is an error")
} else {
var placeMark: CLPlacemark?
placeMark = placemark?[0]
// City
if let city = placeMark?.locality {
self.userCity = city as String
}
// Country
if let country = placeMark?.isoCountryCode {
self.userCountry = country as String
}
}
}
}
And I use these "country" and "city" in "order button" like example;
orderHasBeenCalled = true
buttonLabelText.text = "CANCEL/EDIT"
imgView.image = UIImage(named: "...")
let orderRequestDictionary: [String:Any] = ["..."]
databaseREF.child(userCountry).child(userCity).child(userID!).setValue(orderRequestDictionary)
it works flawlessly user can send order, delete it even when user logout it deleted too, (the whole codes did not included)
now the problem is I want to check if the users have an order when the viewDidLoad() loads for this I am using
if let userID = FirebaseAuth.Auth.auth().currentUser?.uid {
databaseRef.child(userCountry).child(userCity).queryOrdered(byChild: userID!).observe(.childAdded, with: { (snapshot) in
self.OrderHasBeenCalled = true
self.buttonLabelText.text = "CANCEL/EDIT"
self.imgView.image = UIImage(named: "...")
databaseRef.child(self.userCountry).child(self.userCity).removeAllObservers()
})
}
Now the problem is as I read in internet reversegeocode is asynchronous or something like that and as seems it is not ready when the viewDidLoad() load, "code for check if there is order" crash the app because it finds no value to search the names in childs.
Terminating app due to uncaught exception 'InvalidPathValidation', reason: '(child:) Must be a non-empty string and not contain '.' '#' '$' '[' or ']''
To make use of userCountry and userCity in orderbutton I define them before viewDidLoad()
var userCountry = String()
var userCity = String()
I have tried many ways but didn't really figure it out that how can I get reversegeocode completion in viewdidload(). I tried viewDidAppear() too btw but it gives userCountry() and userCity() nil too.
I hope my question is clear and easly understandable. Will be very appreciated if answers will be in that way. Did a lot of researh in the internet some I try, some I did not understand or did not know how can I even try. The last place that my hope shine is stack overflow. Thanks by now for all the people whose Kindly responds my question.
I would change a little the approach. Once working with async functions you must avoid sync request values.
There are several ways to make nested calls from async functions, from your code I arrived at this approach, adapt to your need and it should work.
/////////attention the scope (must be above class declaration)
typealias CompletionGetAddress = (_ userCity : String?, _ userCountry: String?, _ success: Bool) -> Void
var userLocation = CLLocationCoordinate2D()
var locationManager = CLLocationManager()
//
class viewController: ... {
func viewDidLoad() {
yourLocationManager.requestLocation()
// you should implement some code to ensure your userLocation from your locationManager is not nil and a valid location
if let userID = FirebaseAuth.Auth.auth().currentUser?.uid {
if self.userCity != "" {
databaseRef.child(userCountry).child(userCity).queryOrdered(byChild: userID!).observe(.childAdded, with: { (snapshot) in
self.OrderHasBeenCalled = true
self.buttonLabelText.text = "CANCEL/EDIT"
self.imgView.image = UIImage(named: "...")
databaseRef.child(self.userCountry).child(self.userCity).removeAllObservers()
})
} else {
getAddress { (city, country, success) in
if success {
self.userCity = city
self.userCountry = country
databaseRef.child(userCountry).child(userCity).queryOrdered(byChild: userID!).observe(.childAdded, with: { (snapshot) in
self.OrderHasBeenCalled = true
self.buttonLabelText.text = "CANCEL/EDIT"
self.imgView.image = UIImage(named: "...")
databaseRef.child(self.userCountry).child(self.userCity).removeAllObservers()
})
}
}
}
}
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations [0]
if let coord = manager.location?.coordinate {
userLocation = coord /// attention here
}
}
func getAddress(completion: #escaping CompletionGetAddress) {
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(userLocation) {(placemark, error) in
if error != nil {
print("there is an error")
completion(nil, nil, false)
} else {
var city: String = ""
var country: String = ""
var placeMark: CLPlacemark?
placeMark = placemark?[0]
// City
if let c = placeMark?.locality {
city = c
}
// Country
if let c = placeMark?.isoCountryCode {
country = c
}
completion(city, country, true)
}
}
}

Function runs after block of code even though it is called before

Ok I don't get this. I have written some code for forward geocoding, I have an UITextField that you write name of a city in and after you press the enter button it is dismissed and at the same time the function is called to determine if the UITextField contains a valid input. If there is an error it is saved in a bool variable which value is changed in the function. I have print statements all over the place and from the console output I can see that the function ran after the if condition, but it is called before... what? Can somebody explain me what is going on? Code:
var locationError: Bool?
func textFieldShouldReturn(textField: UITextField) -> Bool {
self.view.endEditing(true)
forwardGeocoding(textField.text!)
print("forward geocoding ran 1st time")
print(locationError)
if locationError == true {
print("Error")
} else if locationError == false {
print("Success")
} else if locationError == nil {
print("No value for locationError")
}
return false
}
func forwardGeocoding(address: String) -> CLLocation? {
var userLocation: CLLocation?
CLGeocoder().geocodeAddressString(address, completionHandler: { (placemarks, error) in
if error != nil {
print("Geocoding error: \(error)")
self.locationError = true
return
}
if placemarks?.count > 0 {
print("Placemark found")
self.locationError = false
let placemark = placemarks?.first
let location = placemark?.location
let coordinate = location?.coordinate
print("Settings location: \(coordinate!.latitude), \(coordinate!.longitude)")
if let unwrappedCoordinate = coordinate {
let CLReadyLocation: CLLocation = CLLocation(latitude: unwrappedCoordinate.latitude, longitude: unwrappedCoordinate.longitude)
userLocation = CLReadyLocation
}
}
})
return userLocation
}
Console output:
forward geocoding ran 1st time
nil
No value for locationError
Placemark found
Settings location: 48.8567879, 2.3510768
Try multi threading it...
let queue = NSOperationQueue()
queue.addOperationWithBlock() {
NSOperationQueue.mainQueue().addOperationWithBlock() {
}
}
You need to add the completion handler as your function parameter:
func forwardGeocoding(address: String, completionHandler: (placemarks: String? or [Array of any type], error: NSError?) -> ()) -> CLLocation?
modify your if block:
if error != nil {
print("Geocoding error: \(error)")
self.locationError = true
completionHandler(nil, error)
return
}
then call it like
forwardGeocoding(textField.text!){(placemarks, error) in
//your code.
}
When you call geocodeAddressString, it is executed in a separate thread (#AppleDoc This method submits the specified location data to the geocoding server asynchronously and returns). So effectively you have 2 threads running in parallel. geocodeAddressString in thread 2 will take more time to execute since it makes a server call and the block will be executed when the call returns. During this time thread 1 will finish its execution and will print the log statements.
If you want to handle this issue, locationError if-else condition logic should be implemented in such a way that it should be triggered once your callback is executed.

How to get GMSPlace for a CLLocationCoordinate2D?

I have a location coordinate in the form of a CLLocationCoordinate2D. How do I get its equivalent GMSPlace object using the Google Maps SDK?
This seems like it should be a very simple task, but I couldn't find anything in Google's documentation or on Stack Overflow.
I am working on a similar issue, and I haven't found the exact solution but these alternatives may work depending on your situation. If you are okay with having a GMSAddress instead of a GMSPlace, you may use a GMSGeocoder with a call to reverseGeocodeCoordinate as seen in option two below.
Two options if you're trying to get the user's current location:
Use Google Maps current location to get a GMSPlace. This is pretty simple and solves your problem if you are okay with only resorting to actual places. The problem with this is that I couldn't figure out how to get all addresses (as opposed to businesses). You can see the documentation here.
In viewDidLoad:
let placesClient = GMSPlacesClient()
When you want to get the current place:
placesClient?.currentPlaceWithCallback({ (placeLikelihoods, error) -> Void in
if error != nil {
// Handle error in some way.
}
if let placeLikelihood = placeLikelihoods?.likelihoods.first {
let place = placeLikelihood.place
// Do what you want with the returned GMSPlace.
}
})
Use OneShotLocationManager to get the CLLocationCoordinate2D and turn it into a GMSAddress. You will have to replace the _didComplete function with the code below to return a GMSAddress instead of a CLLocationCoordinate2D.
private func _didComplete(location: CLLocation?, error: NSError?) {
locationManager?.stopUpdatingLocation()
if let location = location {
GMSGeocoder().reverseGeocodeCoordinate(location.coordinate, completionHandler: {
[unowned self] (response, error) -> Void in
if error != nil || response == nil || response!.firstResult() == nil {
self.didComplete?(location: nil,
error: NSError(domain: self.classForCoder.description(),
code: LocationManagerErrors.InvalidLocation.rawValue,
userInfo: nil))
} else {
self.didComplete?(location: response!.firstResult(), error: error)
}
})
} else {
self.didComplete?(location: nil, error: error)
}
locationManager?.delegate = nil
locationManager = nil
}
Someone posted on here a convenient wrapper to extract fields from the GMSAddressComponents that you may find useful when dealing with this API. This makes it easy because then when you want to access the city all you have to do is place.addressComponents?.city as an example.
extension CollectionType where Generator.Element == GMSAddressComponent {
var streetAddress: String? {
return "\(valueForKey("street_number")) \(valueForKey(kGMSPlaceTypeRoute))"
}
var city: String? {
return valueForKey(kGMSPlaceTypeLocality)
}
var state: String? {
return valueForKey(kGMSPlaceTypeAdministrativeAreaLevel1)
}
var zipCode: String? {
return valueForKey(kGMSPlaceTypePostalCode)
}
var country: String? {
return valueForKey(kGMSPlaceTypeCountry)
}
func valueForKey(key: String) -> String? {
return filter { $0.type == key }.first?.name
}
}

Parse Query not working at first properly iOS Swift

Im retrieving parse data to my app depending on the user location.
When i start app first sometimes it Prints "No City" but if i go to another view and comeback then it works properly.
also sometimes it shows me the result the otherway around
example : Only show results that where city name is "sydney" but it shows result that city name is not "sydney"
here is my code
func loadData(){
LiveFeedData.removeAllObjects()
var findLiveFeedData:PFQuery = PFQuery(className: "Spreads")
findLiveFeedData.whereKey("location", equalTo: city)
findLiveFeedData.whereKey("country", equalTo: country)
if city != ""{
findLiveFeedData.findObjectsInBackgroundWithBlock{
(objects:[AnyObject]?, error:NSError?)->Void in
if error == nil{
if let objects = objects as? [PFObject] {
for object in objects{
self.LiveFeedData.addObject(object)
}
}
let array:NSArray = self.LiveFeedData.reverseObjectEnumerator().allObjects
self.LiveFeedData = NSMutableArray(array: array)
self.tableView.reloadData()
}else{
println("Error")
}
}
}else{
println("No City")
}
}
override func viewDidAppear(animated: Bool) {
spinner.startAnimating()
//Location
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
self.loadData()
}
What you probably want to do is cal your loadData method in your delegate method that handles the reception of a new location, that way you can know that when the query gets called, it's only getting run when you know that both the city and the state are populated and not nil.

Resources