ReverseGeocodeLocation completions usage in viewDidLoad() swift4.1 - ios

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)
}
}
}

Related

How do i translate my swift datatype into a realm object

I have developed a little GPS tracking app in Swift which I started as a runtime project to build the parsing logic. I'm now wanting to store the data in Realm objects, but before i go through a refactoring process i wanted to understand whether i'm on the right path.
I currently have a class which has functions to manipulate itself and stores data, this doesn't seem to fit with the various realm models i've seen where by the data object is simply a store of data.
class SkiTrack: NSObject, ObservableObject {
var speed = Speed()
var altitude = Altitude()
var distance = Distance()
var startTime = Date()
var locationManager = CLLocationManager()
var locationList: [Location] = []
private var counter = SkiTrackSegmentCounter()
private var history = LocationHistory()
var trackSegments: [SkiTrackSegment] = []
var currentSegment: SkiTrackSegment
var unknownLocations: [Location] = []
var numberOfLifts: Int = 0
var numberOfRuns: Int = 0
override init() {
self.currentSegment = SkiTrackSegment()
super.init()
self.locationManager.delegate = self
}
public func receiveUpdate(newLocation: CLLocation) {
// Do some management of the location and use it to update data in the class
return
}
private func parse(_ newLocation: CLLocation) -> Location {
let locationInfo = self.history.getCurrentLocationType(speed: newLocation.speed, course: newLocation.course, altitude: newLocation.altitude)
return Location(core: newLocation, altitudeChange: locationInfo.altitudeChange, type: locationInfo.locationType)
}
private func updateDataObjects(location: Location) {
if let lastLocation = self.locationList.last?.core {
let delta = location.core.distance(from: lastLocation)
let isLiftDistance = location.type == .lift
distance.update(Measurement(value: delta, unit: UnitLength.meters), isLift: isLiftDistance)
}
speed.update(location.core.speed)
altitude.update(location.core.altitude)
}
private func clearLocations() {
self.unknownLocations.removeAll()
}
}
// MARK: Delegated Location Receiver
extension SkiTrack: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
for newlocation in locations {
let howRecent = newlocation.timestamp.timeIntervalSinceNow
guard newlocation.horizontalAccuracy < 15 && abs(howRecent) < 5 else {
print("Location Discarded: \(dump(newlocation))")
continue
}
self.receiveUpdate(newLocation: newlocation)
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError: Error) {
print(Error.self)
}
}
My thought was to change the above class into some kind of 'Data Manager' which just manages a simple class that i create, store and retrieve from realm. Is that the approach that i should use with Realm or should i just convert my 'all in one' class into a Realm object?
I'm coming from a SQL background so get classic database models and this is an iteration of an app which used SQLite for data storage in the past. I just don't get the way i'd implement it in realm.
Really appreciate any advice and guidance.

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

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!

GeoFire Swift 3 - Saving and Updating Coordinates

I'm trying to store coordinates into Firebase Database using GeoFire.
I'm unsure how to update the new coordinates as they will be changed/updated every second. With the childByAutoId, it is generating a new unique ID for each Bike.
How do I reference this unique Bike ID? For instance, the user would be called by FIRAuth.auth()?.currentUser?.uid. Is this possible?
let geofireRef = FIRDatabase.database().reference().child("Bike").childByAutoId()
let geoFire = GeoFire(firebaseRef: geofireRef)
var data = geoFire?.setLocation(CLLocation(latitude: userIncrementLat, longitude: userIncrementLong), forKey: "BikeId")
My Firebase Database Structure will look like...
Root
1. Bike
2. UniqueUID Number (Firebase)
3. BikeId
4. g
l
5. 0:
1:
This is my Firebase DB structure for update users location by time and retrive the nearby users to show on map:
db-root
"users" : {
<userUID> : {
"someKey" : "someValue",
...
}
}
"users_location" : {
<userUID> : {
<geofireData> ...
}
}
Vars to use:
let ref = FIRDatabase.database().reference()
let geoFire = GeoFire(firebaseRef: ref.child("users_location"))
To update logged user location:
func updateUserLocation() {
if let myLocation = myLocation {
let userID = FIRAuth.auth()!.currentUser!.uid
geoFire!.setLocation(myLocation, forKey: userID) { (error) in
if (error != nil) {
debugPrint("An error occured: \(error)")
} else {
print("Saved location successfully!")
}
}
}
}
To find nearby user I use the findNearbyUsers function. It' useful to find the nearby users and save into nearbyUsers array the UID key of the the users. The observeReady function is executed after the query completion and uses the UID to retrieve the users details (I use this to show users details on map).
func findNearbyUsers() {
if let myLocation = myLocation {
let theGeoFire = GeoFire(firebaseRef: ref.child("users_location"))
let circleQuery = theGeoFire!.query(at: myLocation, withRadius: radiusInMeters/1000)
_ = circleQuery!.observe(.keyEntered, with: { (key, location) in
if !self.nearbyUsers.contains(key!) && key! != FIRAuth.auth()!.currentUser!.uid {
self.nearbyUsers.append(key!)
}
})
//Execute this code once GeoFire completes the query!
circleQuery?.observeReady({
for user in self.nearbyUsers {
self.ref.child("users/\(user)").observe(.value, with: { snapshot in
let value = snapshot.value as? NSDictionary
print(value)
})
}
})
}
}
Hope it helps

Swift Properties

So, while learning swift, I've run into an issue with adding values to an array property. When I try to print the first value of the array after adding a value to it, I receive an index out of bounds error. How do I add a value to an array property that is accessible to the entire class?
class HomeViewController: UIViewController {
var geofences = [Geofence]()
override func viewDidLoad() {
super.viewDidLoad()
getFences()
print(self.geofences[0])
}
func getFences() {
var query = PFQuery(className:"Geofence")
query.whereKey("username", equalTo: "Peter")
query.findObjectsInBackgroundWithBlock {
(fences: [PFObject]?, error: NSError?) -> Void in
if error == nil && fences != nil {
if let fences = fences {
for (index, element) in fences.enumerate() {
var unique_id = element.objectId
var fence_radius = element["radius"] as! Int
var fence_name = element["name"] as! String
var lat = element["centerPoint"].latitude
var lon = element["centerPoint"].longitude
var center = CLLocationCoordinate2D(latitude: lat, longitude: lon)
var new_fence: Geofence? = Geofence(uniqueID: unique_id!, radius: fence_radius, centerPoint: center, name: fence_name)
self.geofences.append(new_fence!)
}
}
} else {
print(error)
}
}
}
EDIT: It seems I oversimplified the issue. Here is the code that's getting the index out of bounds error. When I retrieve the geofence from Parse, the geofences array is populated, but once it exits the getFences method, the array is emptied.
It's likely that your print call is being run before getFences() has had time to populate the array. You can check this with another print call outside of query.findObjectsInBackgroundWithBlock

iOS Simulator Custom Location issues

I am having issues with the iOS simulator and specifically the custom location setting for the iPhone. When I run the app the first time the simulator is opened it finds the location of the user without issues, however if I then change the custom location, and run the app again it gives the same location as the first time, despite having changed the custom location. If instead I set the Debug> Location > none in the simulator, and change the location in Product > Schemes > Edit Schemes in xCode itself, I have no issues. However every time I change the location this way I have to first set the location to none in the simulator. Is it a problem with my code, or just a quirk of the simulator that I wouldn't find with a real iPhone?
import UIKit
import CoreLocation
import MapKit
var userLocationCity : String!
var userLocationDate : String!
var safeUsername : String!
class TinderViewController: UIViewController, CLLocationManagerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
PFGeoPoint.geoPointForCurrentLocationInBackground { (geopoint: PFGeoPoint!, error: NSError!) -> Void in
if error == nil {
println(geopoint)
var longitude :CLLocationDegrees = geopoint.longitude
var latitude :CLLocationDegrees = geopoint.latitude
var location = CLLocation(latitude: latitude, longitude: longitude) //changed!!!
println(location)
var formatter: NSDateFormatter = NSDateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
let stringDate: String = formatter.stringFromDate(NSDate())
userLocationDate = stringDate
println(userLocationDate)
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
if error != nil {
println("Reverse geocoder failed with error" + error.localizedDescription)
return
}
if placemarks.count > 0 {
println(userLocationCity)
let pm = placemarks[0] as CLPlacemark
println(pm.locality)
println(userLocationCity)
userLocationCity = pm.locality
println(userLocationCity)
user["location"] = userLocationCity
user.save()
let string1 = PFUser.currentUser().objectId
let string2 = "ID_"
safeUsername = string2 + string1
var locate = PFObject(className: safeUsername)
locate.setObject(userLocationCity, forKey: "location")
locate.setObject(userLocationDate, forKey: "date")
locate.saveInBackgroundWithBlock {
(success: Bool!, error: NSError!) -> Void in
if success == true {
println("Score created with ID: \(locate.objectId)")
} else {
println(error)
}
}
}
else {
println("Problem with the data received from geocoder")
}
})
// user["location"] = geopoint
// user.save()
}
}
}
Yes, it sounds like the issue is that you are using two different methods to simulate location. You should choose either to simulate location via schemes or via the debug menu in XCode, but not through both. It sounds like you're doing both, and the setting in the debug menu is overriding the setting in your scheme.
I would strongly advise you, however, to test any location based code on an actual device. Most of the problems that you will find with location services will not appear on the simulator; you really need to deal with the actual peculiarities of real-world GPS hardware.

Resources