I am new to Firebase and not sure how to best explain this but I will try.
I am trying to have my app create an entry for each user. Then each user entry has multiple (0 through n) sub-entries where each sub-entry is a simple string. Basically there is a user-id (the main entry) and their tasks are the sub-entries.
Now my problem is whenever I push data (the sub-entries) to the main entries, all of the previous sub-entries are deleted and only the most recent one is pushed. I have been looking through the documentation and Googling like crazy but nothing seems to work.
I have tried this:
#IBAction func testWrite(sender: AnyObject) {
let def = NSUserDefaults.standardUserDefaults()
let uid = def.valueForKey("uid")
let root = Firebase(url: getFirebaseURL())
let text = self.tempText.text!
let dataRef = root.childByAppendingPath(uid as! String)
let data = ["test": String(text)]
dataRef.setValue(data)
}
Which appends to the user-id entry fine, with a key of "test" and a value of the 'text'
So then I kill the app and change it to:
#IBAction func testWrite(sender: AnyObject) {
let def = NSUserDefaults.standardUserDefaults()
let uid = def.valueForKey("uid")
let root = Firebase(url: getFirebaseURL())
let text = self.tempText.text!
let dataRef = root.childByAppendingPath(uid as! String)
let data = ["CHANGED": String(text)]
dataRef.setValue(data)
}
And it pushes fine, but then the previous entry was just deleted and the only entry left is this one.
What I am trying to do is maybe incrementally (having a numbered key possibly?) add items one by one without having other entries deleted.
I hope this makes sense :P and any help is greatly appreciated!
What is happening here is, you are setting the entire branch (Users/UserID##), to a value, and that value is a single node Changed:<somestring>
Conceptually, it may help to think of the key you want to set as being just another branch e.g (Users/UserID##/TaskID##)
So conceptually, instead of approaching it like this:
Users/UserID = Key:Value
Approach it like this:
Users/UserID/Key = Value
Note: the branch Users/UserID/Key does not have to exist prior to you assigning it a value.
e.g you could change your reference to point at the subkey you want to add or change:
let dataRef = root.childByAppendingPath(uid as! String + "/Task001")
dataref.setValue(String(text))
I concur that what you are doing is a great way to start learning Firebase, and how it works. But once you get going, instead of generating and using your own key for your list of subtasks, do look into childByAutoId, it will automatically create the subkeys for you, plus much more, and is much easier to manage and code.
Documentation Here
Edit: Suggest referring to Frank's better answer below.
An alternative to #MtlDev's answer would be to use updateChildValues():
let data = ["CHANGED": String(text)]
dataRef.updateChildValues(data)
While setValue() replaces the current data with the new value, updateChildValues() updates it in place.
See the Firebase documentation on updating saved data.
Related
If I am storing employee data in firebase which has 3 fields and in future if I want add or remove a field then how physical it is in firebase ? and how can I do it?
I want to use Firebase for one of my iOS project(Swift 4) which I am going to start working on soon so I want to clear the above mentioned concern.
While Lance's answer works - here's a simple alternative. Assume you want to remove an age 'field' from Firebase. Given a structrue
employees
employee_0
name: "Steve"
age: "42"
employee_1
name: "Hank"
age: "37"
if you want to remove the age node from all employee nodes, this will do it
let employeesRef = Database.database().reference().child("employees")
employeesRef.observe(.childAdded, with: { snapshot in
let key = snapshot.key
let refToRemove = employeesRef.child(key).child("age")
refToRemove.removeValue()
})
Adding a field (child node) is even simpler as child node values cannot be nil so when you write the data, it's added to the node
employeesRef.child(employee_key).child("favorite_food").setValue("pizza")
will add a favorite food node with a value of pizza to an employee node.
Note that there are no 'fields' in Firebase, only parent and child nodes which are key: value pairs.
Jay’s answer is the better way to accomplish what you are asking but this is an alternative. If your a beginner this might be easier to understand and start with. As you advance use Jay’s code.
Seems like you want to do something like this.
Your database structure in Firebase is:
root
|
#-012345 // eg. employeeId
|-"firstName": "Hello"
|-"lastName": "World"
|-"age": "25"
You want to add or remove a field there are several ways to do it but this way it's easy. All you have to to is keep track of the original values then add those plus the new values to a Dictionary and exclude whatever you don't want to send to Firebase using the .setValue() method :
// class properties
let rootRef: DatabaseReference? = Database.database().reference()
let employedID = "012345"
let firstName = "Hello" // original value you want to keep
let lastName = "World" // original value you want to keep
let age = "25" // original value you want to REMOVE
lat gender = "female" // NEW value you want ADDED
// press the button to send the write the values to Firebase
#IBAction fileprivate func updateEmployeeButton(_ sender: UIButton) {
// add the values to a dictionary
let dict = [String:Any]()
dict.(firstName, forKey: "firstName")
dict.(lastName, forKey: "lastName")
dict.(gender, forKey: "gender")
let employeeIdRef = rootRef?.child(employedID)
employeeIdRef?.setValue(dict) // call Firebase's .setValue method and pass the dict as an argument
}
Your database structure will now look like this:
root
|
#-012345
|-"firstName": "Hello"
|-"lastName": "World"
|-"gender": "female"
Notice the way I’m proposing complete overwrites what was at that node but Jay’s way keeps what was there but still gets you the changes you want to make.
I have a firebase query that observes data from a posts child.
func fetchPosts () {
let query = ref.queryOrdered(byChild: "timestamp").queryLimited(toFirst: 10)
query.observe(.value) { (snapshot) in
for child in snapshot.children.allObjects as! [DataSnapshot] {
if let value = child.value as? NSDictionary {
let post = Post()
let poster = value["poster"] as? String ?? "Name not found"
let post_content = value["post"] as? String ?? "Content not found"
let post_reveals = value["Reveals"] as? String ?? "Reveals not found"
post.post_words = post_content
post.poster = poster
post.Reveals = post_reveals
self.postList.append(post)
DispatchQueue.main.async { self.tableView.reloadData() }
//make this for when child is added but so that it also shows psots already there something like query.observre event type of
}
}
However, when a user posts something, it creates a more than one cell with the data. For instance, if I post "hello", a two new cards show up with the hello on it. However, when I exit the view and recall the fetch posts function, it shows the correct amount of cells. Also, when I delete a post from the database, it adds a new cell as well and creates two copies of it until I reload the view, then it shows the correct data from the database.
I suspect this has something to do with the observe(.value), as it might be getting the posts from the database and each time the database changes it creates a new array. Thus, when I add a new post, it is adding an array for the fact that the post was added and that it now exists in the database, and when I refresh the view it just collects the data directly from the database.
Also, sometimes the correct amount of cells show and other times there's multiple instances of random posts, regardless of whether I have just added them or not.
How can I change my query so that it initially loads all the posts from the database, and when some post is added it only creates one new cell instead of two?
Edit: The logic seeming to occur is that when the function loads, it gets all the posts as it calls the fetchPosts(). Then, when something is added to the database, it calls the fetchPosts() again and adds the new data to the array while getting all the old data. yet again.
One thing I always do when appending snapshots into an array with Firebase is check if it exists first. In your case I would add
if !self.postList.contains(post) {
self.postList.append...
however, to make this work, you have to make an equatable protocol for what I'm guessing is a Post class like so:
extension Post: Equatable { }
func ==(lhs: Post, rhs: Post) -> Bool {
return lhs.uid == rhs.uid
}
You are right in thinking that the .value event type will return the entire array each time there is a change. What you really need is the query.observe(.childAdded) listener. That will fetch individual posts objects rather than the entire array. Call this in your viewDidAppear method.
You may also want to implement the query.observe(.childRemoved) listener as well to detect when posts are removed.
Another way would be to call observeSingleEvent(.value) on the initial load then add a listener query.queryLimited(toLast: 1).observe(.childAdded) to listen for the latest post.
I've found millions of examples and tutorials on how to obtain a FirDatabaseQuery object (or DatabaseQuery as its now been renamed to) using one of the query....() methods.
But not one of these examples then goes on to show what you can do with the query once you have obtained it.
If you use one of the query methods to obtain a subset of the data as a DatabaseQuery, how do you actually access the set of data objects that it represents?
If you can't, for example, iterate through the query results like you can iterate through a snapshot, then what is a DatabaseQuery actually used for then?
I think that the answer to this is easily found in the docs.
I think that in the Android version of the class it is explained pretty well:
The Query class (and its subclass, DatabaseReference) are used for reading data. Listeners are attached, and they will be triggered when the corresponding data changes.
I can explain it in my own words and more practical like this:
You can use the DatabaseQuery object like a DatabaseReference just with the limitation that only the observe functions work and none of the methods used to manipulate the value, i.e. setValue() etc., of the reference nor any methods related to other nodes, i.e. parent and child (child() etc.), exist.
(I took the code from the docs, so please be free to optimize it/bring in your coding style)
This is how you get data from a DatabaseReference:
var postRef: DatabaseReference!
postRef = Database.database().reference().child("posts")
refHandle = postRef.observe(DataEventType.value, with: { (snapshot) in
let postDict = snapshot.value as? [String : AnyObject] ?? [:]
// ...
})
And this is how you get data from a DatabaseQuery:
// My top posts by number of stars
let myTopPostsQuery = (ref.child("user-
posts").child(getUid())).queryOrdered(byChild: "starCount")
refHandle = myTopPostsQuery.observe(DataEventType.value, with: { (snapshot) in
let postDict = snapshot.value as? [String : AnyObject] ?? [:]
// ...
})
As you can see the second part remains the same, as I mentioned, so in retrieving data you handle a DatabaseQuery like a DatabaseQuery. But I have to agree with you on the point that the iOS especially Swift docs are not that well made, the explanations for the other languages are way clearer.
I would like to save and retrieve features to and from Firebase into a TableView.
The child I would like to save them under is the uid (unique user id)
so a feature would look like this in the database:
Firebase database
The ideal situation, is how the "derde" is saved, so the uid as a key and "derde" as the value.
#IBAction func saveButtonPressed(sender: AnyObject) {
let featureContents = addFeatureTextField.text
if featureContents != "" {
// Build the new Feature.
let newFeature: String = featureContents!
let ref = DataService.dataService.FEATURE_REF.childByAppendingPath(uid)
ref.setValue(newFeature)
where uid is a String, retrieved from authdata somewhere else in the code.
If I save it like this, it saves it to the specific uid path. If I want to add another feature by clicking on the + in the TableViewController, it saves it to the same path, so the Firebase database is updated with the new value and so instead of two features you only end up with one updated feature.
You can prevent this by working with the chilByAutoId() method, to save a list of items. The code would look like this:
#IBAction func saveButtonPressed(sender: AnyObject) {
let featureContents = addFeatureTextField.text
if featureContents != "" {
// Build the new Feature.
let newFeature: String = featureContents!
let ref = DataService.dataService.FEATURE_REF.childByAutoId().childByAppendingPath(uid)
ref.setValue(newFeature)
via this way, a feature is saved, as you can see in the above image at: "vierde"
This allows you to save multiple features with all the same uid, but different autoId.
But, if I save it like this, my tableView stays empty. The TableViewController is like this:
DataService.dataService.FEATURE_REF.observeEventType(.Value, withBlock: { snapshot in
// The snapshot is a current look at our features data.
print("The features in the tableView should be \(snapshot.value)")
self.features = []
if let snapshots = snapshot.children.allObjects as? [FDataSnapshot] {
for snap in snapshots {
// Make our features array for the tableView.
if let postDictionary = snap.value as? String {
print("All in")
let key = snap.key
let feature = Feature(key: key, value: postDictionary)
// Items are returned chronologically, but it's more fun with the newest features first.
self.features.insert(feature, atIndex: 0)
}
}
}
// Be sure that the tableView updates when there is new data.
self.tableView.reloadData()
})
}
Problem lies in this code: if let postDictionary = snap.value as? String {
This conditional binding does not succeed, because the value is not a String, but the autoId key has no value, only the child under it which is the uid has a value "vierde"
Two possible solutions which I am asking you guys:
1) How can I save multiple features with the same uid without using the autoId?
2) If I am obliged to use the autoId, how can I make sure it observes the value of the uid key under the autoId, instead of the non existing value of the autoId.
Thanks for your help!
I think the answer to the question is to build a dictionary out of the key:value pairs of data and store that as a child of your uid node
let featureDict = [ "feature_0": "cool feature", "feature_1": "great feature"]
let ref = DataService.dataService.FEATURE_REF.childByAppendingPath(uid)
ref.setValue(featureDict)
results in
the_uid
feature_0: "cool feature"
feature_1: "great feature"
The limitation here is the key's names, and then the ability to add even more data about each feature.
Here's a potentially better option
the_uid
auto_id_0
feature_name: #"cool feature"
summary: "Everything you'd ever want to know about this feature"
auto_id_1
feature_name: #"great feature"
summary: "Info about this great feature"
The auto_id_x is generated by autoId and allows you to add however many features you want, change their names and summaries. etc. The children of each auto_id_x are (or could be) stored in a dictionary and saved per the above example.
The code is from a book. In terms of overall app architecture (MVC), it's part of the Model. The model has two main components:
An array of tags called tags
A dictionary of tag - query called searches
The app saves these pieces of data in the NSUserDefaults (iOS defaults system) and on iCloud. The following method is called when a change in iCloud is signaled. The parameter is an instance of NSNotification.userInfo
// add, update, or delete searches based on iCloud changes
func performUpdates(userInfo: [NSObject: AnyObject?]) {
// get changed keys NSArray; convert to [String]
let changedKeysObject = userInfo[NSUbiquitousKeyValueStoreChangedKeysKey]
let changedKeys = changedKeysObject as! [String]
// get NSUbiquitousKeyValueStore for updating
let keyValueStore = NSUbiquitousKeyValueStore.defaultStore()
// update searches based on iCloud changes
for key in changedKeys {
if let query = keyValueStore.stringForKey(key) {
saveQuery(query, forTag: key, saveToCloud: false)
} else {
searches.removeValueForKey(key)
tags = tags.filter{$0 != key}
updateUserDefaults(updateTags: true, updateSearches: true)
}
delegate.modelDataChanged() // update the view
}
}
My question is on the if - else inside the for loop. The for loop iterates over keys that where changed; either the user adds a new search, updates an existing search, or deletes a search. But, I don't understand the logic behind the if-else. Some clarifying thoughts would be appreciated. I've read it over and over but it doesn't tick with me.
if let query = keyValueStore.stringForKey(key)
means that if keyValueStore contains a string corresponding to key, then this string will be assigned to the constant query.
This is called "safe unwrapping":
inside the if let ... condition, the query is safely saved with saveQuery because using if let ... guarantees that the value of keyValueStore.stringForKey(key) won't be nil.
If the value is nil, then in the else branch, the filter method is used to update the tags array without the key we just processed: tags.filter{$0 != key} means "return all items in tags that are different from key" (the $0 represents the current item from the array processed by filter).