1

I'm building an app using Firebase that displays a list of Questions in a table view. Each table view cell holds the text of the question, the name of the user that posted it, and the profile photo of the user that posted it.

Here is how I've structured my JSON tree:

"questions"
    "firebase_autoID_01"
        "name": "Jim"
        "text": "Why is the earth round?"
        "userID": "123456"
"userInfo"
    "userID": "123456"
    "photoURL": "gs://default_photo"

I'm trying to retrieve the name and text for each question and the photoURL of the user with the corresponding userID to then place those 3 elements in each table view cell. So I'm trying to retrieve data from the questions child node AND the separate userInfo child node to put that data into the same table view cell. Here is the code I've been trying to do that with:

When a user first gets created, I set their photoURL to a default image that I manually uploaded to Firebase Storage:

...
    let placeholderPhotoRef = storageRef.child("Profile_avatar_placeholder_large.png")
    let placeholderPhotoRefString = "gs://babble-8b668.appspot.com/" + placeholderPhotoRef.fullPath
    //let placeholderPhotoRefURL = NSURL(string: placeholderPhotoRefString)

    let data = [Constants.UserInfoFields.photoUrl: placeholderPhotoRefString]
    self.createUserInfo(data)
}

func createUserInfo(data: [String: String]) {
    configureDatabase()
    let userInfoData = data
    if let currentUserUID = FIRAuth.auth()?.currentUser?.uid{
        self.ref.child("userInfo").child(currentUserUID).setValue(userInfoData)
    }
}

Issues start occurring when I try to retrieve data from both the questions and userInfo:

var photoUrlArray = [String]()

func configureDatabase() {
    ref = FIRDatabase.database().reference()
    _refHandle = self.ref.child("questions").observeEventType(.ChildAdded, withBlock: {(snapshot) -> Void in
        self.questionsArray.append(snapshot)
        //unpack the userID from the "questions" child node to indicate which user to get the photoURL from in the "userInfo" child node
        if let uid = snapshot.value?[Constants.QuestionFields.userUID] as? String {
            self._photoURLrefHandle = self.ref.child("userInfo").child(uid).observeEventType(.ChildAdded, withBlock: {(snapshot) -> Void in
                if let photoURL = snapshot.value as? String {
                    self.photoUrlArray.append(photoURL)
                        self.tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: self.questionsArray.count-1, inSection: 0)], withRowAnimation: .Automatic)
                }
            })
        }
    })
}

I get the following error in the Xcode console:

"Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (0), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'"

What I'm doing here is creating a firebase handle to observe/retrieve the users photoURL from the userInfo child, and that handle is created inside the closure of the handle that observes/retrieves the data from the questions child.

Questions: If this is not the right way to retrieve the data I want, how should I approach this? And how should I structure my JSON data if it isn't currently structure the right way?

Here is how I'm unpacking the name and text from the questions child node in the table view and it's working fine:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell: UITableViewCell! = self.tableView.dequeueReusableCellWithIdentifier("tableViewCell", forIndexPath: indexPath)
    //unpack question from database
    let questionSnapshot: FIRDataSnapshot! = self.questionsArray[indexPath.row]
    var question = questionSnapshot.value as! Dictionary<String, String>
    let name = question[Constants.QuestionFields.name] as String!
    let text = question[Constants.QuestionFields.text] as String!
    cell!.textLabel?.text = name + ": " + text

    return cell!
}

Question: should I unpack the photoURL here or in the previous configureDatabase() function?

alexisSchreier
  • 677
  • 1
  • 5
  • 21

2 Answers2

0

Since you asked for suggestion's on how to store your database structure in JSON. :-

Suggested JSON Structure : -

  {"Users" : { 
        "userID_1" : {
              userName : "Jim",
              userImageURL : "gs://default_photo", 
              questionsAsked : {
                  "questionID_1" : "QUESTION TEXT",
                   "questionID_2" : "QUESTION TEXT"
                }
              } 

        "userID_2" : {
              userName : "Moriarty",
              userImageURL : "gs://default_photo", 
              questionsAsked : {
                  "questionID_1" : "QUESTION TEXT",
                   "questionID_2" : "QUESTION TEXT"
             } 
            }
           }
          } 

Since you are aiming to display the image and name of the user who posted the question with the question itself , i think it would be best if you created the child of questionAsked in the usersId itself that way you can keep track of the no and what questions were asked by that particular user.

Saving the user's profile data would go like this : -

 FIRDatabase.database().reference().child("Users").child(FIRAuth.auth()!.currentUser()!.uid).setValue([
  userName : "Jim",
  userImageUrl : //URL that you want to save
])
//Instead of FIRAuth.auth()!.currentUser()!.uid you can use childByAutoId too but i wont recommend that.

When user makes a post call a function with this code : -

 let rootRef = FIRDatabase.database().reference().child("Users").child(FIRAuth.auth()!.currentUser()!.uid).child("questionsAsked")

   rootRef.observeEventType(.Value, withBlock: {(snap) in

                if snap.exists(){

                    if let questionsDictionary = snap.value as? NSMutableDictionary{

                        questionsDictionary.setObject("Why is the earth round?", forKey: //your questionID)
                        rootRef.setValue(questionsDictionary)
                }
             }else{

                  rootRef.setValue([
  your_questionID : "Why is the earth round?"
       ])

  }
})

How to retrieve a question :-

 let parentRef = FIRDatabase.database().reference().child("Users")

 parentRef.observeEventType(.Value, withBlock: {(userSnap) in

   if userSnap.exists(){
            for eachUser in userSnap.value as! [String:AnyObject]{

                let userId = eachUser.key as! String
                parentRef.child(userId).observeEventType(.Value, withBlock: {(userSnap) in

               if let userInfo = userSnap.value as? [String:AnyObject]{
                    let userNameRecieved = userInfo["userName"] as! String
                    let userImageUrlRecieved = userInfo["userImageURL"] as! String


                 parentRef.child(userId).child("questionsAsked").observeEventType(.Value, withBlock: {(usersQuestion) in
               if userQuestion.exists(){
                      for eachQ in userQuestion.value as! [String:AnyObject]{
                 let question = eachQ.value as! String
               //Create a function named - "initialiseCell"  , that will initialise the datasource of the tableView with username, photoURL and question as its : String parameters
                           initialiseCell(question,userNameRecieved,userImageUrlRecieved)
             }
            }
          })
        }
      })
    } 
  }
})

As for yourQuestion_ID : -

let yourQuestionID = "\(Int(NSDate.timeIntervalSinceReferenceDate() * 1000))

This will produce an unique yourQuestionID every next second or maybe millisecond

This retrieve function in this stage, calls for every question in your entire apps database but you can filter it by modifying the code further...

Dravidian
  • 9,945
  • 3
  • 34
  • 74
  • Dravidian, I really appreciate the detailed answer :) I've been playing around with different JSON data structures to see which one works best in my app's case. IMHO, I would rather keep the `questions` and `users` children separate to avoid nesting data and keep a flatter data structure, as recommended in the Firbase docs: https://firebase.google.com/docs/database/ios/structure-data I'll post what my JSON tree now looks like in an answer below. Thanks again for your help though. – alexisSchreier Aug 16 '16 at 18:30
0

This is how I ended up structuring my Firebase JSON tree:

"users":
    "userID4321":
        "displayName": "bob"
        "photoURL": "gs://default_photo"

"questions":
    "questionID5678":
        "text": "How are you?"
        "userID": "userID4321"

I then implemented the following code in my configureDatabase method:

func configureDatabase() {
    ref = FIRDatabase.database().reference()
    _refHandle = self.ref.child("questions").observeEventType(.ChildAdded, withBlock: {[weak self] (questionSnapshot) in
        let questionID = questionSnapshot.key
        var question = questionSnapshot.value as! [String: AnyObject]
        question[Constants.QuestionFields.questionID] = questionID
        let userID = question[Constants.QuestionFields.userID] as! String

        let usersRef = self?.ref.child("users")
        usersRef?.child(userID).observeEventType(.Value, withBlock: { (userSnapshot) in
            var user = userSnapshot.value as! [String: AnyObject]
            let photoURL = user[Constants.UserFields.photoUrl] as! String
            let displayName = user[Constants.UserFields.displayName] as! String

            question[Constants.QuestionFields.photoUrl] = photoURL
            question[Constants.QuestionFields.displayName] = displayName
...

The observeEventType handle on the questions child gives me a snapshot of each question object every time a new child gets added or a new question gets created. I then create a local dictionary from that question snapshot that was retrieved from the Firebase server. With the userID from that question, I can then identify the corresponding user with another observeEventType method and retrieve that user's photoURL and append it to the question snapshot.

Once all the data I need is in the local question dictionary, I append it to my questionsArray which gets sent to the table view to display each question with the appropriate data.

Let me know if anyone who reads this would have done it differently!

alexisSchreier
  • 677
  • 1
  • 5
  • 21