Function duplicating in Swift causing figure to double inappropriately - ios

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()
}

Related

How to force firebase functions to execute before continuing swift

I am trying to query data from firebase inside a for loop, my problem is since the queries take time to connect, swift is jumping over the queries and coming back later to do them. This creates the problem where my loop counter is ticking up but the queries are being saved for later, when the queries finally do get executed, the counter variable is all out of wack.
Where the code is being skipped is right after the query, where I am trying to append to an array.
func getSelectedData() {
var exerciseIndex = 0
for i in 0...Master.exercises.count - 1 {
if Master.exercises[i].name == self.exerciseName {
exerciseIndex = i
}
}
let numOfSets = Master.exercises[exerciseIndex].totalSets
// For each date record
for count in 0...self.returnedExercises.count-1 {
// Creates a new dataSet
dataSet.append(dataSetStruct())
dataSet[count].date = returnedExercises[count]
for number in 0...(numOfSets - 1) {
// Retrives the reps
let repsDbCallHistory = db.collection("users").document("\(userId)").collection("ExerciseData").document("AllExercises").collection(exerciseName).document(returnedExercises[count]).collection("Set\(number + 1)").document("reps")
repsDbCallHistory.getDocument { (document, error) in
if let document = document, document.exists {
// For every document (Set) in the database, copy the values and add them to the array
let data:[String:Any] = document.data()!
self.dataSet[count].repsArray.append(data["Reps\(number + 1)"] as! Int)
}
else {
// error
}
}
//Retrives the weights
let weightsDbCallHistory = db.collection("users").document("\(userId)").collection("ExerciseData").document("AllExercises").collection(exerciseName).document(returnedExercises[count]).collection("Set\(number + 1)").document("weights")
weightsDbCallHistory.getDocument { (document, error) in
if let document = document, document.exists {
// For every document (Set) in the database, copy the values and add them to the array
let data:[String:Any] = document.data()!
self.dataSet[count].weightsArray.append(data["Weight\(number + 1)"] as! Float)
self.updateGraph()
}
else {
// error
}
}
}
}
}
I even tried breaking out the query into another function but this doesn't seem to fix the issue.
Any help is appreciated, thanks.
EDIT:
func getSelectedData() {
if returnedExercises.count > 0 {
// Create a dispatch group
let group = DispatchGroup()
print("Getting Data")
// For each date record
for count in 0...self.returnedExercises.count-1 {
// Creates a new dataSet
self.dataSet.append(dataSetStruct())
self.dataSet[count].date = self.returnedExercises[count]
for number in 0...(self.numOfSets - 1) {
print("At record \(count), set \(number)")
// Enter the group
group.enter()
// Start the dispatch
DispatchQueue.global().async {
// Retrives the reps
let repsDbCallHistory = self.db.collection("users").document("\(self.userId)").collection("ExerciseData").document("AllExercises").collection(self.exerciseName).document(self.returnedExercises[count]).collection("Set\(number + 1)").document("reps")
repsDbCallHistory.getDocument { (document, error) in
if let document = document, document.exists {
// For every document (Set) in the database, copy the values and add them to the array
let data:[String:Any] = document.data()!
self.dataSet[count].repsArray.append(data["Reps\(number + 1)"] as! Int)
print("Getting data: \(number)")
group.leave()
}
else {
// error
}
}
}
group.wait()
print("Finished getting data")
}
}
I tried to simplify the function for now and only have one database call in the function to try the dispatch groups. I am not sure why firebase is doing this but the code never executes the group.leave, the program just sits idle. If I am doing something wrong please let me know, thanks.
This is what the print statements are showing:
Getting Data
At record 0, set 0
At record 0, set 1
At record 0, set 2
At record 1, set 0
At record 1, set 1
At record 1, set 2
print("Getting data: (number)") is never being executed for some reason.
I am thinking that maybe firebase calls are ran on a separate thread or something, which would made them pause execution as well, but that's just my theory
EDIT2::
func getOneRepMax(completion: #escaping (_ message: String) -> Void) {
if returnedOneRepMax.count > 0 {
print("Getting Data")
// For each date record
for count in 0...self.returnedOneRepMax.count-1 {
// Creates a new dataSet
oneRPDataSet.append(oneRepMaxStruct())
oneRPDataSet[count].date = returnedOneRepMax[count]
// Retrives the reps
let oneRepMax = db.collection("users").document("\(userId)").collection("UserInputData").document("OneRepMax").collection(exerciseName).document(returnedOneRepMax[count])
oneRepMax.getDocument { (document, error) in
if let document = document, document.exists {
// For every document (Set) in the database, copy the values and add them to the array
let data:[String:Any] = document.data()!
self.oneRPDataSet[count].weight = Float(data["Weight"] as! String)!
print("Getting data: \(count)")
completion("DONE")
self.updateGraph()
}
else {
// error
}
}
}
}
}
I tried using completion handlers for a different function and it is also not working properly.
self.getOneRepMax(completion: { message in
print(message)
})
print("Finished getting data")
The order that the print statements should go:
Getting Data
Getting data: 0
Done
Getting data: 1
Done
Finished getting data
The order that the print statements are coming out right now:
Getting Data
Finished getting data
Getting data: 1
Done
Getting data: 0
Done
I am not even sure how it is possible that the count is backwards since my for loop counts up, what mistake am I making?
I think what you need are Dispatch Groups.
let dispatchGroup1 = DispatchGroup()
let dispatchGroup2 = DispatchGroup()
dispatchGroup1.enter()
firebaseRequest1() { (_, _) in
doThings()
dispatchGroup1.leave()
}
dispatchGroup2.enter()
dispatchGroup1.notify(queue: .main) {
firebaseRequest2() { (_, _ ) in
doThings()
dispatchGroup2.leave()
}
dispatchGroup2.notify(queue: .main) {
completionHandler()
}

When I call the following method, Firebase continuously updates in increments of 5 instead of just sending one update. Why is this?

I am using Firebase as my backend and I am trying to increase a the number that is currently being held in the database by 5. However, when it is called, the database adds 5 over and over again, so the score goes from 5 to 10 to 15... this is repeated until the app crashes.
Why is this happening?
func changeUserRewardsScore() {
let db = Firestore.firestore()
db.collection("\(Auth.auth().currentUser?.uid ?? "default")").document("score")
.addSnapshotListener { (querySnapshot, error) in
if let e = error {
print("There was an issue retrieving data from Firestore. \(e)")
}
else {
if let data = querySnapshot?.data() {
let myArray = Array(data.values)
let userScore = "\(myArray[0])"
print("userScore = \(userScore)")
self.writeUserScore(score: userScore)
}
}
}
}
func writeUserScore(score: String) {
var myScore = 0
if localData.rewards.freeCookieInCart == true && score == "50" {
myScore = 0
}
else {
myScore = Int(score)!+5
}
let db = Firestore.firestore()
db.collection("\(Auth.auth().currentUser?.uid ?? "default")").document("score").setData(["score":myScore]) {
(error) in
if let e = error {
print("There was an issue saving data to firestore, \(e)")
} else {
print("Successfully saved data.")
DispatchQueue.main.async {
}
}
}
}
Your document listener, when triggered, writes back to the same document that triggered it, so it triggers again with the result of that change. Which starts the whole cycle over again.
It's not clear to me what you expect to happen instead, but if you just want to get the value of the document once, then update it, you should use get() instead of onSnapshot() as illustrated in the documentation. Either that, or set up some state in your object that indicates to your listener when it shouldn't update the document again.

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 ?? ""
}
//....
}

Methods firing out of order

I have updateHeight, updateWeight, and updateBMI methods in my HealthAlgorithm class. I then try to call them in order in ViewController.swift
HealthAlgorithm.swift:
//MARK: Properties
var healthManager:HealthManager?
var kUnknownString = "Unknown"
var bmi:Double?
var height:HKQuantitySample?
var weight:HKQuantitySample?
func updateHeight() {
// 1. Construct an HKSampleType for weight
let sampleType = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeight)
// 2. Call the method to read the most recent weight sample
HealthManager().readMostRecentSample(sampleType!, completion: { (mostRecentHeight, error) -> Void in
if( error != nil )
{
print("Error reading height from HealthKit Store: \(error.localizedDescription)")
return
}
var heightLocalizedString = self.kUnknownString
self.height = mostRecentHeight as? HKQuantitySample
print(self.height)
// 3. Format the height to display it on the screen
if let meters = self.height?.quantity.doubleValueForUnit(HKUnit.meterUnit()) {
let heightFormatter = NSLengthFormatter()
heightFormatter.forPersonHeightUse = true
heightLocalizedString = heightFormatter.stringFromMeters(meters)
}
})
}
func updateBMI(){
if weight != nil && height != nil {
// 1. Get the weight and height values from the samples read from HealthKit
let weightInKilograms = weight!.quantity.doubleValueForUnit(HKUnit.gramUnitWithMetricPrefix(.Kilo))
let heightInMeters = height!.quantity.doubleValueForUnit(HKUnit.meterUnit())
bmi = ( weightInKilograms / ( heightInMeters * heightInMeters ) )
}
print("BMI: ",bmi)
}
I call these methods in ViewController.swift like this:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
HealthAlgorithm().updateHeight()
HealthAlgorithm().updateWeight()
HealthAlgorithm().updateBMI()
}
The problem is that BMI is returned as nil. The reason this is happening is that the updateBMI method is firing before the updateHeight and updateWeight method.
I use print(self.height) right after I define the variable in the updateHeight method, and I use print("BMI: ", bmi) right after I define the bmi variable in the updateBMI method. Since I am calling updateHeight first, print(self.height) should happen before print("BMI: ", bmi), but for some reason, the BMI: nil is getting returned first which makes no sense to me.
The methods are not being called out of order. The problem is that the function completes asynchronously. You need to call dependent code from the completion handler.

Resources