Search code examples
jsonswiftfirebasefirebase-realtime-databasedenormalization

Reading Two Data Models from Firebase Swift


I have two models: a User model and a book Review model. My username in the Review is of type User so that I can retrieve a given review and the user who created it.

I can now retrieve the Review but not the User object with my code. What am I doing wrong?

User Class

class User: NSObject {
    var id: String?
    var name: String?
    var email: String?
    var profileImageUrl: String?
    init(dictionary: [String: AnyObject]) {
        self.id = dictionary["id"] as? String
        self.name = dictionary["name"] as? String
        self.email = dictionary["email"] as? String
        self.profileImageUrl = dictionary["profileImageUrl"] as? String
    }
}

Review Struct

struct Review {
    let id: String
    let bookName: String
    let author: String
    let review: String
    let username: User
    let createdAt: Date
    init(id: String, bookName: String, author: String, review: String, username: User, timestamp: Double) {

        self.id = id
        self.bookName = bookName
        self.author = author
        self.review = review
        self.username = username
        self.createdAt = Date(timeIntervalSince1970: timestamp / 1000)
    }
}

Review Controller

 private func fetchReviews() {
    let reviewRef = Database.database().reference().child("reviews")
    let queryRef = reviewRef.queryOrdered(byChild: "timestamp").queryLimited(toLast: 5)

    queryRef.observe(.value, with: { snapshot in

        var temporaryReviews = [Review]()

        for child in snapshot.children {
            if let childSnapshot = child as? DataSnapshot,
                let dictionary = childSnapshot.value as? [String:Any],
                let user = dictionary["user"] as? [String:Any],
                let id = user["id"] as? String,
                let name = user["name"] as? String,
                let profileImageUrl = user["profileImageUrl"] as? String,
                let url = URL(string: profileImageUrl),
                let bookName = dictionary["bookName"] as? String,
                let author = dictionary["author"] as? String,
                let review = dictionary["review"] as? String,
                let timestamp = dictionary["timestamp"] as? Double {

                let user = User(dictionary: <#T##[String : AnyObject]#> ??? // Cannot use a user object

                let review = Review(id: childSnapshot.key, bookName: bookName, author: author, review: review, username: user, timestamp: timestamp)

                DispatchQueue.main.async {
                    temporaryReviews.insert(review, at: 0)
                    self.reviews = temporaryReviews
                    self.tableView.reloadData()
                }
            }
        }
    })
}

If I remove the user and its associating values from the loop, I get the data in the UI but no username or photoImageURL which I am updating in my view class. The data is already in Firebase. How can I read the data and the user?


Solution

  • It's not clear what is expected of this line

    let user = User(dictionary: <#T##[String : AnyObject]#> ???
    

    and also not sure why class User: NSObject is an NSObject but this will work with Swift 5.

    class User { //using pure Swift, remove NSObject
        var id: String?
        var name: String?
        var email: String?
        var profileImageUrl: String?
        init(dictionary: [String: Any]) { //change AnyObject to Any
            self.id = dictionary["id"] as? String
            self.name = dictionary["name"] as? String
            self.email = dictionary["email"] as? String
            self.profileImageUrl = dictionary["profileImageUrl"] as? String
        }
    }
    

    then when reading the Firebase data in the closure create a Dictionary of the items you want to populate the user class with

    let id = user["id"] as? String,
    let name = user["name"] as? String,
    let profileImageUrl = user["profileImageUrl"] as? String,
    let url = URL(string: profileImageUrl),
    
    let dict: [String: Any] = [
        "id": id,
        "name": name,
        "email": email,
        "profileImageUrl": profileImageUrl
    ]
    
    let aUser = User(dictionary: dict)
    

    Or you could just pass the snapshot to the User class and let it get whatever it needs from it

    class User {
        var user_id = ""
        var name = ""
        var email = ""
        var profileImageUrl = ""
    
        init(withSnap: DataSnapshot) {
            self.user_id = withSnap.childSnapshot(forPath: "id").value as? String ?? "No Id"
            self.name = withSnap.childSnapshot(forPath: "name").value as? String ?? "No name"
            self.email = withSnap.childSnapshot(forPath: "email").value as? String ?? "No email"
            self.profileImageUrl = withSnap.childSnapshot(forPath: "profileImageUrl").value as? String ?? "No url"
        }
    }
    

    and then

    let aUser = User(withSnap: snapshot)
    

    Also, in your code, the tableView is being reloaded each loop iteration and that may cause flicker. Best bet is to move the self.tableView.reloadData() outside the for loop so it only executes once.