How to save GeoFire with Firebase properties in Swift? - ios

I am trying to save an object that contains a name string, an address string and location coordinates. In the docs it seems that there is one way to save firebase data and another way to save GeoFire data.
Firebase:
var alanisawesome = ["full_name": "Alan Turing", "date_of_birth": "June 23, 1912"]
var gracehop = ["full_name": "Grace Hopper", "date_of_birth": "December 9, 1906"]
var usersRef = ref.childByAppendingPath("users")
var users = ["alanisawesome": alanisawesome, "gracehop": gracehop]
usersRef.setValue(users)
GeoFire:
let geofireRef = Firebase(url: "https://<your-firebase>.firebaseio.com/")
let geoFire = GeoFire(firebaseRef: geofireRef)
geoFire.setLocation(CLLocation(latitude: 37.7853889, longitude: -122.4056973), forKey: "firebase-hq") { (error) in
if (error != nil) {
println("An error occured: \(error)")
} else {
println("Saved location successfully!")
}
}
Is it possible to save both location and other data in the same request? I'd like to do it in the same request because I don't want the user to be able to create an object without location data if the location request fails. Or is there another smart way to impose that restriction?

Since writing the original entity and its geolocation are two separate calls, they will be two separate write operations. One of these writes is done by your code, the other is done by GeoFire. Both calls write to different parts of the JSON tree.
Firebase recently added the ability to write to multiple locations with a single update() call. With that you could write both the geolocation and the entity in one call. If you want to do that, you'll have to change GeoFire to allow for it.

I also struggled for while finding a solution to this, I ended up taking an unorthodox approach. I make my own key title for the GeoFire key entered. Almost like a vin for a car. The first character is a number defining my annotation to use. Next set of numbers is date generated at time of post, after the date is a string of user data because it doesn't matter how long that text is. And that goes as the key. In my maps app the title key is then called and I retrieve the title as a string and cut it up. There are certain characters that are not allowed to be in the GeoFire key title, so you have to restrict the user from typing those.

Related

firestore collection path giving bugs with constants value and String value

So my goal is to get rid of these bugs completely. I am in a dilemma where each decision leads to a bug.
The first thing I can do that eventually becomes an issue is use a String-interpolated collection path in all my query functions like so:
func getEventName() {
listener = db.collection("school_users/\(user?.uid)/events").order(by: "time_created", descending: true).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
if let error = error {
print("There was an error fetching the data: \(error)")
} else {
self.events = querySnapshot!.documents.map { document in
return EventName(eventName: (document.get("event_name") as! String))
}
self.tableView.reloadData()
}
}
}
The thing with this is, when I run the app on the simulator, I am restricted from pressing buttons and then sometimes I can press them and then sometimes they get restricted again. This bug is so confusing because it makes no sense where it springs from.
The other issue is I can use a Constants value in all the query functions in my collections path.
static let schoolCollectionName = "school_users/\(user?.uid)/events"
This is nested in a Firebase struct within the Constants struct. In order to keep Xcode from giving errors I create a let users = Auth.auth().currentUser variable outside the Constants struct. The issue with this value is that when I put that in all of my query functions collection paths, all the buttons are accessible and selectable all the time, but when a user logs out and I log in as a new user, the previous user's data shows up in the new user's tableview.
It would obviously make more sense to use the Constants value because you prevent typos in the future, but I can't figure out how to get rid of the bug where the old user's data shows up in the new user's tableview. Thanks in advance.
The user id should definitely not be a constant. What it sounds like is that right now, you have no reliable way to change users -- your setup probably depends on which user is logged in at app startup, since that's where your variable gets set.
I would do something more like this:
func getEventName() {
guard let user = Auth.auth().currentUser else {
//handle the fact that you don't have a user here -- don't go on to the next query
return
}
listener = db.collection("school_users/\(user.uid)/events").order(by: "time_created", descending: true).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
Note that now, user.uid in the interpolated path doesn't have the ? for optionally unwrapping it (which Xcode is giving you a warning for right now). It will also guarantee that the correct query is always made with the currently-logged-in user.
Regarding being able to press the buttons, that sounds like an unrelated issue. You could run your app in Instruments and check the Time Profiler to see if you have long-running tasks that are gumming up the main/UI thread.

Load existing Geofire location if one exists

I would like Geofire to do one of two things; 1 - Look for a venue location in Firebase and if none exists, 2 - Create one.
So to clarify:
User creates a post based on the current venue.
Geofire checks to see if it already has a venue (Long and Lat cocoordinates) on record for that geolocation. If it DOES have that location stored in Firebase then it takes the user's new post and ADDS it to the same venue.
If Geofire DOES NOT detect a location in Firebase it creates a new venue and attaches the post to it.
I have separated my venue details from my Geofire coordinates but I dont understand how do I get Geofire to differentiate between whether an entry for this location does or does not exist within Firebase.
At the moment when I run my app it creates a new entry every time as per the screen-grab below, which is obviously not what I want.
Here is the code:
First I set the reference for the name and the directory (I set them seperately because I use the name ref in a few instances)
override func viewDidLoad() {
super.viewDidLoad()
geoFireNameRef = Database.database().reference().childByAutoId()
geoFireRef = Database.database().reference().child("PostLocations").child("Coordinates")
geoFire = GeoFire (firebaseRef: geoFireRef)
}
and then in my post method I call:
func savePost () {
let location = CLLocation.init(latitude: lat!, longitude: long!) //Get Coordinates
geoFire.setLocation(location, forKey: self.geoFireNameRef.key)// Save Coordinates
postLocationDB.child(self.geoFireNameRef.key).setValue(locationDictionary) // Save Venue Details
}
So I understand that you want to check if there is a venue with an existing couple of coordinates. What I would suggest is to store this couple under your Coordinates/{venueId} node as an extra field which is composed by the two coordinates, like the following:
- Coordinates
- -LAWR2LH.....
- l
- 0: 22.345
- 1: 22.345
- compoundL: "22.345-22.345"
and then directly query the Real Time Database with a query that combines queryOrderedByChild and queryEqualToValue in order to find if a Coordinates/Venue node with compoundL = 22.345-22.345 is already existing.

Does simply pulling all users from firebase database and putting them into an array efficient?

I have a simple iOS app that part of the app grabs all the users from firebase database so you can search them, and do different functions. Now my question is, if/when the app grows and there are thousands of users, does pulling all the users from the database and adding them to an array of [user]'s, still not crash or slow the app? I see so many people on youtube just loop through firebase and grab all the users. Please note I am excluding profile photos so there is no downloading images involved, just strings. I have some code I thought could solve this possible problem, but I am starting to wonder if there even is a problem with just fetching all the users from firebase and putting them into and array and then displayed in a tableview.
Here is some of my code right now, but it still I notice when I type in one letter, then turn airplane mode on, it downloaded all the users. I really need some help or some advice on this one, thanks.
var checklers = [String]()
func updateSearchResults(for searchController: UISearchController) {
if searchController.searchBar.text == "" {
filteredUsers = users
}
else {
print("refreshing")
if let uidi = FIRAuth.auth()?.currentUser?.uid {
view.addSubview(activityInd)
activityInd.startAnimating()
filteredUsers.removeAll()
checklers.removeAll()
let ref = FIRDatabase.database().reference()
ref.child("users").queryOrderedByKey().observe(.value, with: { snapshot in
if let userr = snapshot.value as? [String : AnyObject] {
for (_, velt) in userr {
if let usernamerr = velt["Username"] as? String {
if usernamerr.lowercased().contains(searchController.searchBar.text!.lowercased()) {
let userNew = usera()
if let name = velt["Full Name"] as? String, let uidd = velt["uid"] as? String {
userNew.name = name
userNew.username = usernamerr
userNew.uid = uidd
if self.checklers.contains(uidd) {
print("already")
}
else {
if userNew.uid != uidi {
self.filteredUsers.append(userNew)
self.activityInd.stopAnimating()
self.checklers.append(uidd)
}
print("added a user")
}
}
}
}
self.tableViewSearchUser.reloadData()
}
}
})
ref.removeAllObservers()
}
// filteredUsers = users.filter( { ($0.username?.lowercased().contains(searchController.searchBar.text!.lowercased()))! })
}
tableViewSearchUser.reloadData()
}
Please add any advice, thanks.
Just for searching one or two users, each time a user would need to fetch all the records and putting them all in an array (all in memory). You want SQL-where query function, but Firebase is just different and doesn't have it.
Problem with storing fetching all data approach:
1) Storing just all the user's information in an array of user objects is NOT scalable on client's device.
2) When the number of users gets to ten of thousands, a day worth of search by a single user will eat up a sizable amount of real time database read quota.
3) Stale user data, an user has to re-download all the users just becauase on user changed his name to Doggie1 to doggieTwo.
Solutions:
1) If you haven't done so already, I suggest the options of doing some server-side filtering first by following the best practice here:
Firebase Retrieving Data - Filtering
Downloading a sub-set of user that fits some criteria and then do a bit of client-side filtering. Still is problematic when users get to tens of thousands.
Firebase has a client-size data persistence feature, but in your case if there filtering rule doesn't fit your need, you need do you own caching with some persistent storage solution. Instead of putting the fetched object in an Array[User], I would store each in a database SQLite on iOS and Android apps.
2) Implement a ElasticSearch with the FlashLight plugin, this involves some extra setup (I know, I've been through it, I learned quite a bit), but it is well worth it for autocomplete and search functions that Firebase currently doesn't support.
A pluggable integration with ElasticSearch to provide advanced content searches in Firebase.

Trouble retrieving data from Firebase using .Value/.Child added in swift

i am working on a project where I am pulling data from firebase (which is "queued"). Essentially data is being saved w/ a time stamp so when it's called, it can be ordered sequentially (first in, first out).
The problem I am facing is when I retrieve the data on my app. From my research, on Stack overflow as well as firebase docs, .Value gives a snapshot and continues to listen to data when new data is added. However when new data is added it will take a new snapshot of the entire set (hence "duplicating data" on my app's array).
To get around this I have tried to instead use .ChildAdded, which works well to add new data to my array when new data is added to the firebase database. However it isn't adding the full data set (data that is already existing in the database), which is what I need in addition to new data being added.
Suppose firebase nodes as such:
App_Queue:
Category1:
Timestamp1:
Uid: User_1_uid
Timestamp2:
Uid: User_2_uid
Swift code (2.3):
Case1:
self.databaseRef.child("App_Queue/\(Category1)").queryLimitedToLast(15).observeEventType(.Value, withBlock: { (snapshot) in
if let userDict = snapshot.value as? [String:AnyObject]{
for each in userDict{
let timeIdExtract = each.0 as! String
self.timeIdArray.append(timeIdExtract)
print(self.timeIdArray)
}
//this gives me full snapshot of time stamps & a userId
//but if timestamp3 and user_3_uid is added to firebase, the array is appended with a new snapshot thus, duplicating items.
Case2:
self.databaseRef.child("App_Queue/\(Category1)").queryLimitedToLast(15).observeEventType(FIRDataEventType.ChildAdded, withBlock: { (snapshot : FIRDataSnapshot) in
if let userDict = snapshot.value as? [String:AnyObject]{
for each in userDict{
let timeIdExtract = each.0 as! String // Every follwers ID.
self.timeIdArray.append(timeIdExtract)
print(self.timeIdArray)
}
//this gives me only new items added, but not already added.
// if timestamp3 and user_3_uid is added the array is appended with this new item. But timestamp1 & timestamp2 not added
Case 3:
I have tried a hybrid of Case1 (instead use .observeSingleEventOfType()) & Case2, by adding a self.databaseRef.removeAllObservers() after the code in case 1, and then turning on the .ChildAdded observer for case2. It almost works.... added initial snapshot via case 1, then listens however if say, timestamp2 is recently added it will append this again when Case2 is called, thus duplicating the array.
So my question: how the heck do I go about getting existing objects in the database AND adding on new ones, without duplicating data in the array?
EDIT
Im silly-- had some extraneous code after the block which was throwing things off. DOH! For anyone experiencing issues, first place to check! also .ChildAdded paired with observeEventType did the trick. (takes all previous data already in DB + adds new data as it comes in).
self.databaseRef.child("App_Queue/\(Category1)").queryLimitedToLast(15).observeEventType(.ChildAdded, withBlock: { (snapshot) in
...
Im silly-- had some extraneous code after the block which was throwing things off. DOH! For anyone experiencing issues, first place to check! also .ChildAdded paired with observeEventType did the trick. (takes all previous data already in DB + adds new data as it comes in).
self.databaseRef.child("App_Queue/\(Category1)").queryLimitedToLast(15).observeEventType(.ChildAdded, withBlock: { (snapshot) in
...

Cloudkit fetch data (strings and image asset) take a long time to appear after call

I was hoping that someone can help a coding newbie with what might be considered a stupid question. I'm making a blog type app for a community organization and it's pretty basic. It'll have tabs where each tab may be weekly updates, a table view with past updates and a tab with general information.
I setup cloudkit to store strings and pictures, and then created a fetchData method to query cloud kit. In terms of the code (sample below) it works and gets the data/picture. My problem is that it takes almost 5-10 seconds before the text and image update when I run the app. I'm wondering if that's normal, and I should just add an activity overlay for 10 seconds, or is there a way to decrease the time it takes to update.
override func viewDidLoad() {
fetchUpcoming()
}
func fetchUpcoming() {
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
let query = CKQuery(recordType: "Upcoming", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil { // There is no error
println(results)
for entry in results {
self.articleTitle.text = entry["Title"] as? String
self.articleBody.text = entry["Description"] as? String
let imageAsset: CKAsset = entry["CoverPhoto"] as! CKAsset
self.articlePicture.image = UIImage(contentsOfFile: imageAsset.fileURL.path!)
self.articleBody.sizeToFit()
self.articleBody.textAlignment = NSTextAlignment.Justified
self.articleTitle.adjustsFontSizeToFitWidth = true
}
}
else {
println(error)
}
}
}
Another question I had is about string content being stored on cloud kit. If I want to add multiple paragraphs to a blood entry (for example), is there a way to put it in one record, or do I have to separate the blog entry content into separate paragraphs? I may be mistaken but it seems like CloudKit records don't recognize line breaks. If you can help answer my questions, I'd be really appreciative.
It looks like you might be issuing a query after creating the data, which isn't necessary. When you save data, as soon as your completion block succeeds (with no errors) then you can be sure the data is stored on the server and you can go ahead and render it to the user.
For example, let's say you're using a CKModifyRecordsOperation to save the data and you assign a block of code to the modifyRecordsCompletionBlock property. As soon as that block runs and no errors are passed in, then you can render your data and images to your user. You have the data (strings, images, etc.) locally because you just sent them to the server, so there's no need to go request them again.
This provides a quicker experience for the user and reduces the amount of network requests and battery you're using on their device.
If you are just issuing normal queries when your app boots up, then that amount of time does seem long but there can be a lot of factors: your local network, the size of the image you're downloading, etc. so it's hard to say without more information.
Regarding the storage of paragraphs of text, you should consider using a CKAsset. Here is a quote from the CKRecord's documentation about string data:
Use strings to store relatively small amounts of text. Although
strings themselves can be any length, you should use an asset to store
large amounts of text.
You'll need to make sure you're properly storing and rendering line break characters between the user input and what you send to CloudKit.

Resources