I have a viewController that is very similar to tinder. A user can swipe left or right depending on how many values are stored in firebase. Every value from firebase shows on the users VC no matter if it matches their preferences or not. I wanted to allow users to have preferences (again, very similar to tinder.) and only values that fall within those limits to show on the viewController. The way I am fetching all of the values from firebase is
func fetchPost() {
topCardView = nil
var previousCardView: CardView?
Database.database().reference().child("Post").observe(.childAdded, with: { (snapshot) in
if let userDictionary = snapshot.value as? [String: AnyObject] {
let poster = Poster(dictionary: userDictionary as [String : AnyObject])
let cardView = self.setupCardFromUser(poster: poster)
self.cardViewModels.append(poster.toCardViewModel())
if let currentUser = Auth.auth().currentUser?.uid,
currentUser == poster.fromId {
cardView.removeFromSuperview()
} else if self.topCardView == nil {
self.topCardView = cardView
}
previousCardView?.nextCardView = cardView
previousCardView = cardView
}
}, withCancel: nil)
}
The code above, allows the user to see every single value from firebase. but the preferences I want to manipulate this is cost and skills.
is there a simple way for me to only show the values from firebase if they match the users preferences?
in firebase, the usersPreferences tree is set up as
users
|______ usersUID
|______ minSeekingCost
|______ maxSeekingCost
|______ skills1
and how the postings are set up look like
post
|____ usersUID
|____ category
|____ cost
I want the users to find postings that are within the min&maxSeekingCost, and match their skills. Say if a post matches one of their skills, and the price is not within their limits, then it is not fetched. Same for if post does not match and the price is in their limits.
would I have to fetch the users preferences inside of the fetchPost? or can I manipulate the fetchPost itself to have these called.
You're looking for queries, which allow you to order and filter data.
But Firebase Database queries can only order/filter on a single property. In certain cases it is possible to combine the values you want to filter on into a single (synthetic) property. For an example of this and other approaches, see my answer here: Query based on multiple where clauses in Firebase
Related
I want to retrieve data on with this value 7hmpcTuCAYQAYRqP7RNmnegSd9r2
But i'm getting all four objects in snapshot. I want to get parent key values which contain this key
7hmpcTuCAYQAYRqP7RNmnegSd9r2
Need to get blue mark keys.
Here is my code
let ref = FirebaseManager.refs.databaseRoot.child("new_ChatListMembers")
ref.queryOrdered(byChild: (Auth.auth().currentUser?.uid)!).queryEqual(toValue: true)
ref.observeSingleEvent(of: .value) { (snapshot) in
print(snapshot)
}
This code return four objects instead of two.Please help me how i can get specific data.
Thanks
You're not actually using the query that you construct in your code. To use that, it'd be something like:
let ref = FirebaseManager.refs.databaseRoot.child("new_ChatListMembers")
let query = ref.queryOrdered(byChild: (Auth.auth().currentUser?.uid)!).queryEqual(toValue: true)
query.observeSingleEvent(of: .value) { (snapshot) in
print(snapshot)
}
But this won't actually scale very well, as you'll need to define an index for each individual UID value in this structure. In short: your current data structure makes it each to find the users for a specific chat room, but it doesn't help finding the chat rooms for a specific user.
To allow the latter, you'll want to add an extra structure in your data:
user_chats: {
"$uid": {
"$chatid": true
}
}
So this is pretty much the inverse of what you have already, which is why this is often called an inverse index.
For more on this, and another example, see my answer here: Firebase query if child of child contains a value
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'm reading data from my NoSQL Firebase database, parsing that data into individual components, then displaying them in my tableView. I've added table refreshing functionality so when a new piece of data is added the user can refresh and it will be added to the table.
The function that's call to refresh the table is the same function that does the initial table populating, so in a sense refreshing just restarts the view. The steps that are taken are:
Empty out array and dictionary that hold parsed data elements
Fetch data from database
Parse that data
Reload the table
Here's the full function:
func readEventsFromDb() {
// 1. Empty out data structures
eventsForDate.removeAll()
allDates.removeAll()
// 2. Fetch data
let dbRef = FIRDatabase.database().reference().child("pets").child(currentPet).child("events")
dbRef.observeSingleEvent(of: .value, with: { snapshot in
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
// 3. Parse data elements
for child in snapshots{
if let data = child.value as? [String: Any] {
if let c = data["comment"] as? String, let p = data["user"] as? String, let t = data["type"] as? Int, let d = data["date"] as? UInt64 {
let event = PetEvent(comment: c, person: p, type: t, time: self.timeFromEpoch(time: Double(d)))
let eventDate = self.dateFromEpoch(time: Double(d))
if (self.eventsForDate[eventDate] != nil) {
self.eventsForDate[eventDate]!.append(event)
} else {
self.eventsForDate[eventDate] = [event]
}
}
}
}
// 4. Refresh table
self.allDates = Array(self.eventsForDate.keys)
self.feedTable.reloadData()
self.refreshControl.endRefreshing()
}
})
}
It doesn't make a lot of sense to me that refreshing the table would pretty much just restart the view, as this is the only thing in the view. Is this how table refreshing usually works or is there a more efficient way to do such a thing?
Use ref.observe instead of ref.observeSingleEvent to continuous updating the table view.
ref.observe(.childAdded ...) //insert row
ref.observe(.childRemoved ...) //remove row
ref.observe(.childChanged ...) //update row
I'm not sure why you would do so much manual work to have the user refresh this data - one of the biggest values of Firebase is that you can do this automatically. This can work, but is definitely not how "most other applications" do this.
I would STRONGLY recommend you take a look at the FirebaseUI project:
https://github.com/firebase/FirebaseUI-iOS
This includes data sources for UITableView and UICollectionView displays that handle 90% of the work behind what you're doing, but also support incremental (and animated, like other iOS apps) row display. If a row is deleted, for instance, the user would see that deletion with a nice animation, while maintaining their scroll position within the table. (The solution you've outlined will lose this position, which isn't very user-friendly.)
Included in the project is a simple example app that uses the module to show a simple live table:
https://github.com/firebase/FirebaseUI-iOS/tree/master/FirebaseDatabaseUITests
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).