Search code examples
iosswiftfirebase-realtime-databaseuikit

Having Trouble Pulling Data From Firebase RT Database


Super new to coding so apologies if something is super obvious here.

I'm working on an app that I can use to keep track of my weight lifting split. I write the data like this:

public func writeNewExercise(splitName: String, day: Int, exerciseNum: Int, exerciseName: String, sets: String, repsSecs: String, isTimed: Bool, completion: @escaping (Bool) -> Void) {
        
        let user = AuthManager.shared.user
        var exerciseRef: DatabaseReference!
        exerciseRef = Database.database().reference(withPath: "\(user.uid)/splits/\(splitName)/day \(day)/exercise \(exerciseNum)")
        
        var dataDictionary: [String: Any] = [:]
        dataDictionary["Exercise Name"] = exerciseName
        dataDictionary["Sets"] = sets
        dataDictionary["Reps or Secs"] = repsSecs
        dataDictionary["Is Timed"] = isTimed
        
        exerciseRef.setValue(dataDictionary) { error, _ in
            if error == nil {
                completion(true)
                return
            } else {
                completion(false)
                return
            }
            
        }
        
    }

This gives me a JSON dictionary in Firebase that looks like this:

{
  "8aIzPgurRLPPEYDpXWv54r5JjvH3" : {
    "splits" : {
      "Test Split" : {
        "day 1" : {
          "exercise 0" : {
            "Exercise Name" : "Curls",
            "Is Timed" : false,
            "Reps or Secs" : "12",
            "Sets" : "4"
          }
        }
      }
    }
  },

What I want to do now is to pull this data so I can insert each exercise into a tableView cell. Don't want to do anything fancy with it -- just be able to view it so I can follow my split. I'm doing this more for practice than practicality. I've tried pulling the data about 15 different ways, and no matter what I do it just won't work. I'm totally stumped. Here is the code I have right now:

    public func downloadPost(splitName: String, day: Int, completion: @escaping (Bool) -> Void){
        
        let user = AuthManager.shared.user
        var exerciseRef: DatabaseReference!
        exerciseRef = Database.database().reference()
        
        var exerciseArray = [Exercise]()
        
        exerciseRef.child("Users").child(user.uid).child("splits").child(splitName).child("day \(day)").observe(.value) { snapshot in
            
            if snapshot.exists(){
                for x in 0...100{
                
                let nameValue = snapshot.childSnapshot(forPath: "exercise \(x)/Exercise Name").value
                let setsValue = snapshot.childSnapshot(forPath: "exercise \(x)/Sets").value
                let repsOrSecsValue = snapshot.childSnapshot(forPath: "exercise \(x)//Sets/Reps or Secs").value
                let isTimedValue = snapshot.childSnapshot(forPath: "exercise \(x)/Sets/Is Timed").value
                
                let exercise = Exercise(name: "\(nameValue!)",
                                        sets: "\(setsValue!)",
                                        repsOrSecs: "\(repsOrSecsValue!)",
                                        isTimed: isTimedValue as? Bool ?? false)
                    print(exercise.name)
                    print(exercise.sets)
                    print(exercise.repsOrSecs)
                    print(exercise.isTimed)
                exerciseArray.append(exercise)
                completion(true)
                return
            }
        } else {
            print("no snapshot exists")
        }
        
        print(exerciseArray)
        
    }
    }

Exercise is a custom class I've created that has a name, amount of sets, amount of reps, and a Bool "isTimed". This code prints:

no snapshot exists, []

Trying other things, I've got it to print something like:

null, 0, 0, false

Some other stuff I've tried has been:

  • using slash navigation instead of chaining .childs in the .observe.value
  • using .getData instead of .observe
  • throwing DispatchQueue.main.async all over the place
  • making the exerciseRef be the whole database, then calling to the specific point when assigning the snapshot.value
  • Much else

I've probably put something like 15 hours into just this at this point, and I really cannot figure it out. Any help would be massively appreciated. I'll watch this post closely and post any info that I may have left out if it's needed.

Thanks!

UPDATE

Got everything working by using the code provided by Medo below. For others trying to do something like this, after pulling the array as Medo demonstrated, just set all the labels in your tableViewCell to ExportedArray[indexPath.row].theClassPropertyYouWant


Solution

  • Here is my solution:

    public func downloadPost(splitName: String, day: Int, completion: @escaping (([Exercise]) -> ())){
        
        let user = AuthManager.shared.user
        var exerciseRef: DatabaseReference!
        exerciseRef = Database.database().reference()
        
        var exerciseArray = [Exercise]()
        
        exerciseRef.child(user.uid).child("splits").child(splitName).child("day \(day)").observe(.value, with: { snapshot in
            
            guard let exercises = snapshot.children.allObjects as? [DataSnapshot] else {
                print("Error: No snapshot")
                return
            }
            
            
            
            for exercise in exercises {
                let exerciseData = exercise.value as? [String:Any]
                
                let exerciseName = exerciseData["Exercise Name"] as? String
                let isTimed = exerciseData["Is Timed"] as? Bool
                let repsOrSecs = exerciseData["Reps or Secs"] as? String
                let sets = exerciseData["Sets"] as? String
                
                let exerciseIndex = Exercise(name: "\(exerciseName)",
                                        sets: "\(sets)",
                                        repsOrSecs: "\(repsOrSecs)",
                                        isTimed: isTimed)
                
                exerciseArray.append(exerciseIndex)
                
            }
          completion(exerciseArray)  
        }
    }
    

    You can call the function downloadPost and extract the array from it like this:

    downloadPost(splitName: "", day: 0, completion: {
        aNewArray in
        
        // aNewArray is your extracted array [Exercise]
        print("\(aNewArray)")
        
    })
    

    Few things to be aware of:

    1. If you want to ensure that your storing your exercises in order (and extract the data in order) then instead of having exercises 0, 1, 2... (in your database), name it by an id called "childByAutoId". Firebase will auto order them for you as you add/push or extract that data. Replace your writeNewExercise function with:

      let user = AuthManager.shared.user
      var exerciseRef: DatabaseReference!
      let key = Database.database().reference().childByAutoId().key ?? ""
      exerciseRef = Database.database().reference(withPath: "\(user.uid)/splits/\(splitName)/day \(day)/\(key)")
      
      var dataDictionary: [String: Any] = [:]
      dataDictionary["Exercise Name"] = exerciseName
      dataDictionary["Sets"] = sets
      dataDictionary["Reps or Secs"] = repsSecs
      dataDictionary["Is Timed"] = isTimed
      
      exerciseRef.setValue(dataDictionary) { error, _ in
          if error == nil {
              completion(true)
              return
          } else {
              completion(false)
              return
          }
      
      }
      
    2. Firebase Realtime Database is a breadth first search and download. So you should probably flatten out your database structure as much as possible. This means observing on exerciseRef.child("Users").child(user.uid).child("splits").child(splitName).child("day \(day)") would still download all the exercise days.