Swift -Firebase how to observe multiple posts in tableView cells - ios

I have a tableView that has a number of cells for each post. I need to observe changes to a couple of different properties (availability && hours) and I would like to update them in the specific cells that they relate to. This is what I came up with but I don't think it is the correct way to do it because I only need to listen for changes to the availability && hours properties.
struct Post {
var postId: String
var uid: String
var availability: Bool // this can change
var hours: String // this can change
}
var arrOfPosts = [Post]() // 1 - n posts
override viewDidLoad() {
super.viewDidLoad()
for post in arrOfPosts {
let postId = post.postId
Database....child(postId).observe( .childChanged, with: { (snapshot) in
let updatedPostId = snapshot.key
if let indexOfItem = self.arrOfPosts.firstIndex(where: { $0.postId == updatedPostId }) {
// update post in cell via batchUpdate
}
})
Database....child(postId).observe( .removed, with: { (snapshot) in
let deletedPostId = snapshot.key
if let indexOfItem = self.arrOfPosts.firstIndex(where: { $0.postId == deletedPostId }) {
// remove cell via batchUpdate
}
})
}
}

It's correct but you need to be specific in path to minimize response load that firebase adds more cost for
Database....child("\(postId)/availability")
same for hours

Related

Firebase database observer not getting all children

I am observing my Firebase database for .childAdded. When I add a child (Dictionary to the database, it should create a snapshot of all the values to parse into a 'Task' within my app.
func storeTask(_ task: Task) {
let taskInfo: [String] = [task.title, task.desc, set, due, "\(task.complete)"]
setTaskDatabaseValue(values: taskInfo, dbChild: dbReference.child(task.title))
}
func setTaskDatabaseValue(values val: [String], dbChild db: DatabaseReference) {
db.child("title").setValue(val[0])
db.child("desc").setValue(val[1])
db.child("set").setValue(val[2])
db.child("due").setValue(val[3])
db.child("complete").setValue(val[4])
}
And this is my observer method:
func getTasks() {
self.dbReference.observe(.childAdded) { (snapshot) in
let snapVal = snapshot.value as! Dictionary<String,String>
let title = snapVal["title"]!
let desc = snapVal["desc"]!
let set = snapVal["set"]!
let due = snapVal["due"]!
let complete = snapVal["complete"]!
let task = Task(title: title, desc: desc, set: set, due: due, complete: (complete == "true"))
TaskController.sharedInstance.addTaskToLocalList(task)
self.tableView.reloadData()
}
}
Where getTasks() is called, in the viewDidLoad() of the view controller :
DatabaseController.sharedInstance.addTableView(tableView)
DatabaseController.sharedInstance.getTasks()
The observer method always seems to only retrieve the task title and nothing else, yet when I check the Firebase console all the information is there. Why is this?
Thanks!

Updating tableview without reloading the view (Swift, Firebase)

So I am trying to get all of the favourites from a database to show up in a tableview. However, when I favourite a user, it only displays one.
The structure of my database looks like:
Favourites:
UserID:
key: User 1
key: User 2
and my code:
var usersArray[UserClass]()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
fetchFavourites()
}
func fetchFavourites () {
let userID = Auth.auth().currentUser?.uid
let favouriteRef = self.databaseRef.child("favourites").child(userID!)
favouriteRef.queryOrderedByKey().observe(.childAdded, with: { (snapshot) in
let favouriteID = "\(snapshot.value!)"
let usersRef = self.databaseRef.child("users")
usersRef.observe(.value, with: { (users) in
self.usersArray.removeAll()
for user in users.children {
let user = UserClass(snapshot: user as! DataSnapshot)
if favouriteID == user.uid {
self.usersArray.append(user)
}
self.tableView.reloadData()
}
})
})
}
I have tried creating another array within the function and then making the main array = to that, however it doesn't update the tableview when switching between screens. This code only works when the page view is loaded for the first time. However, I am using a back button that doesn't up date the screen.
Thanks, let me know if you want me to provide anything else.
Edited code (new problem, see below):
func fetchFavourites () {
DispatchQueue.main.async( execute: {
let userID = Auth.auth().currentUser?.uid
var tempFav = [UserClass]()
let favouriteRef = self.databaseRef.child("favourites").child(userID!)
favouriteRef.queryOrderedByKey().observe(.childAdded, with: { (snapshot) in
let favouriteID = "\(snapshot.value!)"
let usersRef = self.databaseRef.child("users")
tempFav.removeAll()
usersRef.observe(.value, with: { (users) in
self.usersArray.removeAll()
for user in users.children {
let user = UserClass(snapshot: user as! DataSnapshot)
if favouriteID == user.uid {
tempFav.append(user)
}
self.usersArray = tempFav
self.tableView.reloadData()
}
})
})
})
}
The problem has been fixed where it populates the tableview with all of the liked users and gets rid of them if I unlike them etc. However, if I unlike all of the users. The array duplicates a user twice even though it should be empty.

Grab Data, Sort Data, then Load into Table View (Swift 3 : Firebase)

I'm working on an application using Firebase. What I'm trying to accomplish is getting data from Firebase, sorting that data, and then finally loading that data into a tableView once that is done.
I'm not gonna share my whole code, but here's essentially how I want it to work:
var posts = [PostStruct]()
var following = [String]()
...
let databaseRef = FIRDatabase.database().reference()
for follower in following {
databaseRef.child("Posts").child(follower).observe(.value, with: {
DataSnapshot in
//Parse All The Data...
self.posts.insert(...)
}
}
self.posts.sort{$0.date.compare($1.date) == .orderedDescending}
print("Test")
self.tableView.reloadData()
That print("Test") gets called, but it gets called before the FIRDatabase is requested, so that tells me that there is absolutely no data in the tableView when it's sorting. So, I need to find a way to only sort once the Database is finished requesting.
I can put the sort and reload method in the for statement, and that works, but it loads everything up choppy, and it's not very efficient.
Not sure if this is the best way to handle this, but you could add a counter that is incremented and then execute your sort and reload code once that counter is equal to the count of the following array.
var counter = 0
let databaseRef = FIRDatabase.database().reference()
for follower in following {
databaseRef.child("Posts").child(follower).observe(.value, with: {
DataSnapshot in
//Parse All The Data...
counter += 1
self.posts.insert(...)
if counter == following.count {
self.sortPosts()
}
}
}
func sortPosts() {
self.posts.sort{$0.date.compare($1.date) == .orderedDescending}
print("Test")
self.tableView.reloadData()
}
if this is for your youtube tutorials I will try to answer
I think the solution of Donny is going to work, you can do it also with a callback function
func getData(handle:#escaping ((Bool) -> Void)){
let databaseRef = FIRDatabase.database().reference()
for follower in following {
databaseRef.child("Posts").child(follower).observe(.value, with: {
DataSnapshot in
//Parse All The Data...
counter += 1
self.posts.insert(...)
if counter == following.count {
handle(true)
}
}
}
}
and then in your method where you are calling getData.
getData(){ ready in
self.posts.sort{$0.date.compare($1.date) == .orderedDescending}
print("Test")
self.tableView.reloadData()
}

Swift iOS: Firebase Paging

I have this Firebase data:
I want to query the posts data through pagination. Currently my code is converting this JS code to Swift code
let postsRef = self.rootDatabaseReference.child("development/posts")
postsRef.queryOrderedByChild("createdAt").queryStartingAtValue((page - 1) * count).queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
....
})
When accessing, this data page: 1, count: 1. I can get the data for "posts.a" but when I try to access page: 2, count: 1 the returns is still "posts.a"
What am I missing here?
Assuming that you are or will be using childByAutoId() when pushing data to Firebase, you can use queryOrderedByKey() to order your data chronologically. Doc here.
The unique key is based on a timestamp, so list items will automatically be ordered chronologically.
To start on a specific key, you will have to append your query with queryStartingAtValue(_:).
Sample usage:
var count = numberOfItemsPerPage
var query ref.queryOrderedByKey()
if startKey != nil {
query = query.queryStartingAtValue(startKey)
count += 1
}
query.queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
guard var children = snapshot.children.allObjects as? [FIRDataSnapshot] else {
// Handle error
return
}
if startKey != nil && !children.isEmpty {
children.removeFirst()
}
// Do something with children
})
I know I'm a bit late and there's a nice answer by timominous, but I'd like to share the way I've solved this. This is a full example, it isn't only about pagination. This example is in Swift 4 and I've used a nice library named CodableFirebase (you can find it here) to decode the Firebase snapshot values.
Besides those things, remember to use childByAutoId when creating a post and storing that key in postId(or your variable). So, we can use it later on.
Now, the model looks like so...
class FeedsModel: Decodable {
var postId: String!
var authorId: String! //The author of the post
var timestamp: Double = 0.0 //We'll use it sort the posts.
//And other properties like 'likesCount', 'postDescription'...
}
We're going to get the posts in the recent first fashion using this function
class func getFeedsWith(lastKey: String?, completion: #escaping ((Bool, [FeedsModel]?) -> Void)) {
let feedsReference = Database.database().reference().child("YOUR FEEDS' NODE")
let query = (lastKey != nil) ? feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE" + 1).queryEnding(atValue: lastKey): feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE")
//Last key would be nil initially(for the first page).
query.observeSingleEvent(of: .value) { (snapshot) in
guard snapshot.exists(), let value = snapshot.value else {
completion(false, nil)
return
}
do {
let model = try FirebaseDecoder().decode([String: FeedsModel].self, from: value)
//We get the feeds in ['childAddedByAutoId key': model] manner. CodableFirebase decodes the data and we get our models populated.
var feeds = model.map { $0.value }
//Leaving the keys aside to get the array [FeedsModel]
feeds.sort(by: { (P, Q) -> Bool in P.timestamp > Q.timestamp })
//Sorting the values based on the timestamp, following recent first fashion. It is required because we may have lost the chronological order in the last steps.
if lastKey != nil { feeds = Array(feeds.dropFirst()) }
//Need to remove the first element(Only when the lastKey was not nil) because, it would be the same as the last one in the previous page.
completion(true, feeds)
//We get our data sorted and ready here.
} catch let error {
print("Error occured while decoding - \(error.localizedDescription)")
completion(false, nil)
}
}
}
Now, in our viewController, for the initial load, the function calls go like this in viewDidLoad. And the next pages are fetched when the tableView will display cells...
class FeedsViewController: UIViewController {
//MARK: - Properties
#IBOutlet weak var feedsTableView: UITableView!
var dataArray = [FeedsModel]()
var isFetching = Bool()
var previousKey = String()
var hasFetchedLastPage = Bool()
//MARK: - ViewController LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
//Any other stuffs..
self.getFeedsWith(lastKey: nil) //Initial load.
}
//....
func getFeedsWith(lastKey: String?) {
guard !self.isFetching else {
self.previousKey = ""
return
}
self.isFetching = true
FeedsModel.getFeedsWith(lastKey: lastKey) { (status, data) in
self.isFetching = false
guard status, let feeds = data else {
//Handle errors
return
}
if self.dataArray.isEmpty { //It'd be, when it's the first time.
self.dataArray = feeds
self.feedsTableView.reloadSections(IndexSet(integer: 0), with: .fade)
} else {
self.hasFetchedLastPage = feeds.count < "YOUR FEEDS PER PAGE"
//To make sure if we've fetched the last page and we're in no need to call this function anymore.
self.dataArray += feeds
//Appending the next page's feed. As we're getting the feeds in the recent first manner.
self.feedsTableView.reloadData()
}
}
}
//MARK: - TableView Delegate & DataSource
//....
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if self.dataArray.count - 1 == indexPath.row && !self.hasFetchedLastPage {
let lastKey = self.dataArray[indexPath.row].postId
guard lastKey != self.previousKey else { return }
//Getting the feeds with last element's postId. (postId would be the same as a specific node in YourDatabase/Feeds).
self.getFeedsWith(lastKey: lastKey)
self.previousKey = lastKey ?? ""
}
//....
}

Firebase restoring initial values in textfields after changing text value

I am practicing iOS (Swift) with Firebase. the first viewcontroller retrieves all the records from firebase db and populate the tableView from an array. when the user selects an item from that tableview a new viewcontroller pops up segueing the object from the listView viewcontroller to the detail viewcontroller. data is populated to the fields successfully!
however when i try to update any of the textfield, the moment i switch to another textfield the initial value is restored in the edited textfield.
I have tried to removeAllObservers... but nothing worked. i even removed "import Firebase" and all associated objects and still it restores the initial value.
am i missing any concept here?
this is the code from the ListViewController:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated);
self.activityIndicator.startAnimating();
self.observeIngrdUpdates = ref.observeEventType(.Value, withBlock: { (snapshot) in
self.ingredients.removeAll();
for child in snapshot.children {
var nutrition:String = "";
var type:Int = 0;
var desc:String = "";
var img:String = "";
var name:String = "";
var price:Double = 0.0;
if let _name = child.value["IngredientName"] as? String {
name = _name;
}
if let _nutrition = child.value["NutritionFacts"] as? String {
nutrition = _nutrition;
}
if let _type = child.value["IngredientType"] as? Int {
type = _type;
}
if let _desc = child.value["UnitDescription"] as? String {
desc = _desc;
}
if let _img = child.value["IngredientImage"] as? String {
img = _img;
}
if let _price = child.value["IngredientPrice"] as? Double {
price = _price;
}
let ingredient = Ingredient(name: name, type: type, image: img, unitDesc: desc, nutritionFacts: nutrition, price: price);
ingredient.key = child.key;
self.ingredients.append(ingredient);
}
self.tableView.reloadData();
})
}
and the PrepareForSegue:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let destinationVC = segue.destinationViewController as! EditIngredientVC;
if segue.identifier?.compare(SEGUE_EDIT_INGREDIENTS) == .OrderedSame {
destinationVC.ingredient = ingredients[tableView.indexPathForSelectedRow!.row];
destinationVC.controllerTitle = "Edit";
} else if segue.identifier?.compare(SEGUE_ADD_INGREDIENT) == .OrderedSame {
destinationVC.controllerTitle = "New";
}
}
this is the code for populating the fields in DetailViewController:
override func viewDidLayoutSubviews() {
lblControllerTitle.text = controllerTitle;
if controllerTitle?.localizedCaseInsensitiveCompare("NEW") == .OrderedSame {
self.segVegFru.selectedSegmentIndex = 0;
} else {
if ingredient != nil {
self.txtIngredientName.text = ingredient!.name;
self.txtUnitDesc.text = ingredient!.unitDesc;
self.segVegFru.selectedSegmentIndex = ingredient!.typeInt;
self.txtNutritionFacts.text = ingredient!.nutritionFacts;
self.txtPrice.text = "\(ingredient!.price)";
}
}
}
Thank you all for your help.
It's probably because you are putting your textField populating code, in viewDidLayoutSubviews() method.
This method will be triggered every time the layout of a views changes in viewcontroller.
Move it to viewDidLoad() and it should be fine.
The reason that it's being reverted to the previous value is because. You are populating the textField.text from the "ingredient" object. The ingredient will have the same value retained that you passed from previous view controller. Until you mutate it at some point.
And by the way Firebase is very cool I like it too :)

Resources