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 ?? ""
}
//....
}
Related
I'm currently attempting to pull a series of items from my database, and append them into an array stored within a variable.
This works successfully - and once the viewDidLoad executes.
I then want to call a function which iterates through this array, and for each element in it, pull out a balance and add it to a global variable.
I can do this by calling the function inside my initial database call, and wrapping it in DispatchQueue, however - it's duplicating, and actually doubling the value, almost like the add function is being called twice.
But I can't see where this is happening, or why. My understanding is that this database call only occurs once, but it seems like the function is getting called twice.
Particularly, my problem is happening like follows:
totalBalance is equal to 0 while the database call resolves
database call finds two entries, saves them to accounts variable
calculateBalance finds first balance of 2, second balance of 3 and adds together, updating totalBalance variable to 5
calculateBalance gets called again, adding 2 and 3 to totalBalance and equalling 10
totalBalance should equal 5, but it gets 2 + 3 twice, so ends up as being 10.
Here's my code:
class DashboardViewController: UIViewController {
let db = Firestore.firestore()
var accounts: [Account] = []
var totalBalance: Int = 0
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var balanceLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
fetchAccounts()
}
func fetchAccounts() {
db.collection("accounts")
.getDocuments { (QuerySnapshot, error) in
if let err = error {
print("Error getting documents: \(err)")
} else {
if let snapshotDocuments = QuerySnapshot?.documents {
for doc in snapshotDocuments {
let data = doc.data()
if let balance = data["accountBalance"] as? String {
let newAccount = Account(
accountBalance: balance,
)
self.accounts.append(newAccount)
}
DispatchQueue.main.async {
self.calculateBalance()
self.tableView.reloadData()
}
}
}
}
}
}
func calculateBalance() {
for cash in accounts {
totalBalance += Int(cash.accountBalance)!
}
DispatchQueue.main.async {
self.balanceLabel.text = "Overall balance - -£\(self.totalBalance)"
}
}
}
Any feedback here would be really appreciated - been scratching my head but think someone coming in from the outside will be able to spot what my problem is.
If I'm reading this correctly, you're calling the calculateBalance method in a loop, if you had 5 accounts in snapshotDocuments the method would get called 5 times:
for doc in snapshotDocuments {
let data = doc.data()
if let balance = data["accountBalance"] as? String {
let newAccount = Account(
accountBalance: balance,
)
self.accounts.append(newAccount)
}
//This will get called multiple times
//DispatchQueue.main.async {
// self.calculateBalance()
// self.tableView.reloadData()
//}
}
//This will get called only once
DispatchQueue.main.async {
self.calculateBalance()
self.tableView.reloadData()
}
I'm trying to populate the Sections and Rows of my tableview using Firestore data that I've parsed and stored inside of a dictionary, that looks like this...
dataDict = ["Monday": ["Chest", "Arms"], "Wednsday": ["Legs", "Arms"], "Tuesday": ["Back"]]
To be frank, I'm not even sure if I'm supposed to store the data inside of a dictionary as I did. Is is wrong to do that? Also, since the data is being pulled asynchronously, how can I populate my sections and rows only after the dictionary is fully loaded with my network data? I'm using a completion handler, but when I try to print the results, of the dataDict, it prints out three arrays in succession, like so...
["Monday": ["Chest", "Arms"]]
["Tuesday": ["Back"], "Monday": ["Chest", "Arms"]]
["Tuesday": ["Back"], "Monday": ["Chest", "Arms"], "Wednsday": ["Legs", "Arms"]]
Whereas I expected it to return a single print of the array upon completion. What am I doing wrong?
var dataDict : [String:[String]] = [:]
//MARK: - viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
vcBackgroundImg()
navConAcc()
picker.delegate = self
picker.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
tableView.tableFooterView = UIView()
Auth.auth().addStateDidChangeListener { (auth, user) in
self.userIdRef = user!.uid
self.colRef = Firestore.firestore().collection("/users/\(self.userIdRef)/Days")
self.loadData { (done) in
if done {
print(self.dataDict)
} else {
print("Error retrieving data")
}
}
}
}
//MARK: - Load Data
func loadData(completion: #escaping (Bool) -> ()){
self.colRef.getDocuments { (snapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
completion(false)
}
else {
//Appending all Days collection documents with a field of "dow" to daysarray...
for dayDocument in snapshot!.documents {
self.daysArray.append(dayDocument.data()["dow"] as? String ?? "")
self.dayIdArray.append(dayDocument.documentID)
Firestore.firestore().collection("/users/\(self.userIdRef)/Days/\(dayDocument.documentID)/Workouts/").getDocuments { (snapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
completion(false)
}
else {
//Assigning all Workouts collection documents belonging to selected \(dayDocument.documentID) to dictionary dataDict...
for document in snapshot!.documents {
if self.dataDict[dayDocument.data()["dow"] as? String ?? ""] == nil {
self.dataDict[dayDocument.data()["dow"] as? String ?? ""] = [document.data()["workout"] as? String ?? ""]
} else {
self.dataDict[dayDocument.data()["dow"] as? String ?? ""]?.append(document.data()["workout"] as? String ?? "")
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
// print(self.dataDict)
}
completion(true)
}
}
}
self.dayCount = snapshot?.count ?? 0
}
}
}
I think it is just the flow of your program. Every time you loop through the collection, you add what it gets to the dictionary. So on the first pass, it will print that the dictionary has 1 item. On the second pass, it adds another item to the dictionary, and then prints the dictionary, which now has 2 items in it, so 2 items are printed. I don't think you are seeing unexpected behavior, it is just how you have your log statement ordered with how you are looping.
In other words, it makes sense that it is printing like that.
I agree with #ewizard's answer. The problem is in the flow of your program. You iterate through the documents and you fetch the documents in the collection for every iteration. You also reload the tableView and call completion closure multiple times, which you don't want to do.
To improve the flow of your program try using DispatchGroup to fetch your data and then put it together one once, when all the data is fetched. See example below to get the basic idea. My example is a very simplified version of your code where I wanted to show you the important changes you should preform.
func loadData(completion: #escaping (Bool) -> ()) {
self.colRef.getDocuments { (snapshot, err) in
// Handle error
let group = DispatchGroup()
var fetchedData = [Any]()
// Iterate through the documents
for dayDocument in snapshot!.documents {
// Enter group
group.enter()
// Fetch data
Firestore.firestore().collection("/users/\(self.userIdRef)/Days/\(dayDocument.documentID)/Workouts/").getDocuments { (snapshot, err) in
// Add your data to fetched data here
fetchedData.append(snapshot)
// Leave group
group.leave()
}
}
// Waits for until all data fetches are finished
group.notify(queue: .main) {
// Here you can manipulate fetched data and prepare the data source for your table view
print(fetchedData)
// Reload table view and call completion only once
self.tableView.reloadData()
completion(true)
}
}
}
I also agree with other comments that you should rethink the data model for your tableView. A much more appropriate structure would be a 2d array (an array of arrays - first array translates to the table view sections and the inner array objects translate to section items).
Here's an example:
// Table view data source
[
// Section 0
[day0, day1],
// Section 1
[day2, day3],
// Section 2
[day4, day5],
]
Example of usage:
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].count
}
}
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
I'm running into a weird issue where my tableView is reloading too early after retrieving JSON data. The strange thing is sometimes it reloads after getting all the required data to fill the tableView and other times it reloads before it can acquire the data. I'm not entirely sure why it's doing this although I do notice sometimes the data is returned as nil. Here is what I use to retrieve the data:
var genreDataArray: [GenreData] = []
var posterStringArray: [String] = []
var posterImageArray: [UIImage] = []
override func viewDidLoad() {
super.viewDidLoad()
GenreData.updateAllData(urlExtension:"list", completionHandler: { results in
guard let results = results else {
print("There was an error retrieving genre data")
return
}
self.genreDataArray = results
for movie in self.genreDataArray {
if let movieGenreID = movie.id
{
GenrePosters.updateGenrePoster(genreID: movieGenreID, urlExtension: "movies", completionHandler: {posters in
guard let posters = posters else {
print("There was an error retrieving poster data")
return
}
for poster in posters {
if let newPoster = poster {
if self.posterStringArray.contains(newPoster){
continue
} else {
self.posterStringArray.append(newPoster)
self.networkManager.downloadImage(imageExtension: "\(newPoster)",
{ (imageData)
in
if let image = UIImage(data: imageData as Data){
self.posterImageArray.append(image)
}
})
break// Use to exit out of array after appending the corresponding poster string
}
} else {
print("There was a problem retrieving poster images")//This gets called sometimes if the poster returns nil
continue
}
}
})
}
}
DispatchQueue.main.async {
self.genresTableView.reloadData()//This is reloading too early before the data can be retrieved
}
})
}
The data is being retrieved asynchronously, and thus your table view can sometimes reload without all the data. What you can do is have the table view reload at the end of the asynchronous data retrieval, or you can reload the cells individually as they come in instead of the whole table using
let indexPath = IndexPath(item: rowNumber, section: 0)
tableView.reloadRows(at: [indexPath], with: .top)
TRY THIS-:
var genreDataArray: [GenreData] = []
var posterStringArray: [String] = []
var posterImageArray: [UIImage] = []
override func viewDidLoad() {
super.viewDidLoad()
genredataArray.removeAll()
posterStringArray.removeAll()
posterImageArray.removeAll()
NOW HERE CALL YOUR CLASS FUNCTION AS ABOVE
}
I guess in that case, you should use
dispatch_async(dispatch_get_main_queue(),{
for data in json as! [Dictionary<String,AnyObject>]
{
//take data from json. . .
}
//reload your table -> tableView.reloadData()
})
You should get the main queue of the thread.
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()
}