1

My main question is how to get rid of the flicker, but I also just want to know if I am working with denormalized Firebase data correctly, and most efficiently. Is my approach anywhere near being correct?

So I am struggling with trying to properly display the data from a firebase database with data that's been denormalized. I have posts, and then comments associated with each post. Every time somebody opens up the comments section of a post, by segueing from a viewcontroller to a new viewcontroller, it grabs the unique key for the post (postKey), and then scans the group of comments associated with the postKey contained in the postCommentGroup. The group of comments, which are children of each postKey in the postCommentGroup are just the commentKey as key and "true" as the value, which indicates which comments are associated with which posts. The comments are in an entirely different branch as that is what I think the Firebase documentation suggests one should do.

I essentially have 3 layers of nested observers.

For the sake of clarity I'm recycling the cells with dequeuereusablecells in a tableview, and I also have a rudimentary lazy load/image caching mechanism that might be interfering with things too, but I have the same mechanism on other less complicated tableviews so I don't think that's the problem.

Due to my lack of knowledge I don't know how else to display the data other than going through this cycle. I think this cycle, may be causing the flicker, but I don't know how else to make it load the data. I have tried various other ways of doing it, such as using a query, but I've never been able to get it to work.

As a side note, I've tried to get up to speed on how to query data (which I assume might help me), but there's been an update to the syntax of Swift, and also an update to Firebase, making the previous examples a little difficult to follow.

Also, I just can't find good, recent examples of properly using denormalized data in a somewhat complex way in any of the Firebase documentation, either on the Firebase site or on Github. Does anyone know of good reference material to look at with regard to working with denormalized data using Swift 3.0 and Firebase (newest version - not the legacy version), whether it's a project on GitHub, or a blog, or just a collection of the most useful posts on stackoverflow?

Here is the firebase data structure:

   
"comments" : {
        "-KaEl8IRyIxRbYlGqyXC" : {
          "description" : "1",
          "likes" : 1,
          "postID" : "-KaEfosaXYQzvPX5WggB",
          "profileImageUrl" : "https://firebasestorage.googleapis.com",
          "timePosted" : 1484175742269,
          "userID" : "9yhij9cBhJTmRTexsRfKRrnmDRQ2",
          "username" : "HouseOfPaine"
        }
      },
     
      "postCommentGroup" : {
        "-KaEfosaXYQzvPX5WggB" : {
          "-KaEl8IRyIxRbYlGqyXC" : true,
          "-KaEl9HiPCmInE0aJH_f" : true,
          "-KaF817rRpAd2zSCeQ-M" : true
        },
        "-KaF9ZxAekTEBtFgdB_5" : {
          "-KaFEcXsSJyJwvlW1w2u" : true

        },
        "-KaJyENJFkYxCffctymL" : {
          "-KaQYa0d08D7ZBirz5B4" : true
        }
      },
      "posts" : {
        "-KaEfosaXYQzvPX5WggB" : {
          "caption" : "Test",
          "comments" : 11,
          "imageUrl" : "https://firebasestorage.googleapis.com/",
          "likes" : 0,
          "profileImageUrl" : "https://firebasestorage.googleapis.com/",
          "timePosted" : 1484174347995,
          "title" : "test",
          "user" : "17lIDKNx6LgzQmaeQ2ING582zi43",
          "username" : "Freedom"
        }
      },

Here is my code:

func commentGroupObserver() {

    DataService.ds.REF_POST_COMMENT_GROUP.observeSingleEvent(of: .value, with: { (snapshot) in

        if snapshot.value != nil {

            if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] , snapshots.count > 0 {

                self.comments = []

                for snap in snapshots {

                    if let tempVarPostKeyForCommentGroup = snap.key as String? {

                        if tempVarPostKeyForCommentGroup == self.post.postKey {

                            self.postKeyForCommentGroup = tempVarPostKeyForCommentGroup

                            self.commentObservers()
                        } else {

                        }
                    } else {

                    }
                }

            }

        } else {
            print("error")
        }

    })


}


func commentObservers() {

    if postKeyForCommentGroup != nil {

        constantHandle = DataService.ds.REF_POST_COMMENT_GROUP.child(postKeyForCommentGroup).observe(.value, with: { (snapshot) in

            if snapshot.value != nil {

                if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot], snapshots.count > 0

                {

                    self.comments = []

                    for snap in snapshots {

                        if let theCommentIDForEachComment = snap.key as String? {
                             DataService.ds.REF_COMMENTS.child(theCommentIDForEachComment).queryOrdered(byChild: "timePosted").observeSingleEvent(of: .value, with: { (snapshots) in

                                if let commentDict = snapshots.value as? Dictionary<String, AnyObject> {

                                    let key = snapshots.key
                                    let comment = Comment(commentKey: key, dictionary: commentDict)
                                    self.comments.insert(comment, at: 0)     
                                }
                                self.tableView.reloadData()
                            })
                        }
                    } 
                }

            } else {

            }
        })

    } else {

    }

}

UPDATE:

I figured out how to use queries and a delegate pattern outlined in a previous stackoverflow post:

getting data out of a closure that retrieves data from firebase

But I don't know if I am using the delegate pattern correctly.

The code has been simplified by using the query, but it is still flickering. Maybe I am not using the delegate pattern correctly?

    func commentGroupObserver() {
    DataService.ds.REF_POST_COMMENT_GROUP.queryOrderedByKey().queryStarting(atValue: post.postKey).queryEnding(atValue: post.postKey).observeSingleEvent(of: .value, with: { (snapshot) in
        self.postKeyForCommentGroup = self.post.postKey
        self.commentObservers()
    })

}

func commentObservers() {
    if postKeyForCommentGroup != nil {
        constantHandle = DataService.ds.REF_POST_COMMENT_GROUP.child(postKeyForCommentGroup).observe(.value, with: { (snapshot) in
            if snapshot.value != nil {
                if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot]
                {
                    self.comments = []
                    for snap in snapshots {
                        if let theCommentIDForEachComment = snap.key as String? {
                            DataService.ds.REF_COMMENTS.child(theCommentIDForEachComment).queryOrdered(byChild: "timePosted").observe(.value, with: { (snapshots) in

                                if let commentDict = snapshots.value as? Dictionary<String, AnyObject> {

                                    let key = snapshots.key
                                    let comment = Comment(commentKey: key, dictionary: commentDict)
                                    self.comments.insert(comment, at: 0)

                                }

                                self.didFetchData(comments: self.comments)

                            })



                        }

                    }

                }

            } else {

            }
        })

    } else {

    }

}

func didFetchData(comments data:[Comment]){
    self.tableView.reloadData()
}

}

And the protocol

 protocol MyDelegate{
func didFetchData(comments:[Comment]) }

The code on my end which solved it:

Upon Jay's suggestion I eliminated the unnecessary postCommentGroup and just queried the UID of the post the comment belongs to under the comment:

    func commentObservers() {

    let queryRef = DataService.ds.REF_COMMENTS.queryOrdered(byChild: "postID").queryEqual(toValue: self.post.postKey)

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

        if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {

            for snap in snapshots {

                if let commentDict = snap.value as? Dictionary<String, AnyObject> {
                    let key = snap.key
                    let comment = Comment(commentKey: key, dictionary: commentDict)
                    self.comments.insert(comment, at: 0)
                }
            }
        }

        self.tableView.reloadData()
    })
}
Community
  • 1
  • 1
jasonhdev
  • 33
  • 6
  • 1
    Please remove the pics of your data structure and replace it with textual data. That way it is copy and paste-able and can be searched. To export your JSON Structure, go to the Firebase console and to your database then from the 'three vertical dots' in the right corner inside the window, select export JSON. – Jay Jan 13 '17 at 18:45
  • You may be double loading your data as well (which would cause flicker) as you read it once with .value and you are reading all of the same data again with .childAdded. However, that not be the case - not enough code in the question to know. – Jay Jan 13 '17 at 18:53
  • I removed the image and uploaded an edited version of my data (JSON Structure). I removed the second observe with the childAdded because it was confusing to people who might see this question. I had previously added it to test out different ways of loading the data. The removal of that second observe with the childAdded had no impact on the flickering, and it was flickering before I added it. – jasonhdev Jan 14 '17 at 08:15
  • I've been trying to implement various ways of querying the data, but I think because my data structure relies on auto-ids for many of the parent values to sort things I haven't been successful. I've been playing around with DispatchGroups. They looked promising, but I've been unsuccessful thus far. Thanks for your suggestion. Have a good day. – jasonhdev Jan 14 '17 at 08:18
  • Move the *self.tableView.reloadData()* out of the for loop. Call it after the loop completes populating the array, otherwise if you have 50 items the tableView is being refreshed 50 times when it really only needs it once. – Jay Jan 14 '17 at 13:39
  • Also, when adding the observer to the postKeyForCommentGroup, it's by .value. That means any time there's a change in that node, the entire node is loaded again. That may be what you want, if not, using .childAdded, .childChanged, and .childRemoved may be a better solution as those notify the app of the respective event and only pass in the node that was modified. – Jay Jan 14 '17 at 14:25
  • If I move self.tableView.reloadData() outside of the closure then the tableview isn't populated with Data. So I googled a little more and found this stackoverflow answer: http://stackoverflow.com/questions/38364288/getting-data-out-of-a-closure-that-retrieves-data-from-firebase so I am playing around with the delegate pattern outlined in that post. In addition to that I'm also trying to play with querying data in the first function, the commentGroupObserver to simplify things, and just to see if I can do it. Btw, thanks for your response. We haven't found solution yet, but it does help. – jasonhdev Jan 14 '17 at 16:56
  • Not the closure; move it outside the *for loop*. In other words, populate your dataSource array with the for loop, then when its done (still inside the closure) reload the tableView. You probably don't need the delegate pattern as your code is ok - just need to tighten it up. – Jay Jan 14 '17 at 17:03
  • I managed to figure out how to use queries, and I think I'm using the delegate pattern correctly. The flicker is not gone, but the code has been simplified somewhat. See update in original post. – jasonhdev Jan 14 '17 at 17:07
  • I understand it has to be outside the for loop. I just don't understand how to do that and also keep it in the closure because the for loop is where the last closure is getting the keys for each comment that it should use for that particular post using the var theCommentIdForEachComment. – jasonhdev Jan 14 '17 at 17:19

2 Answers2

4

Your approach may need to be adjusted by simplifying. I wanted to provide all of the nuts and bolts so it's a bit lengthy and could in itself be simplified.

While denormalizing is normal, it's not a requirement and in some cases can add additional complexity. The 'layer' in your structure postCommentGroup, appears to be unneeded.

It looks like you have a view controller containing posts, and a second view controller that displays the comments when the user taps a post on the first controller.

You really only need a posts node and a comments node

posts
   post_id_0
     title: "my post title"
     caption: "some caption"
     uid: "uid_0"
   post_id_1
     title: "another post title
     caption: "another caption
     uid: "uid_0"

and a comments node that references the post

comments
   comment_0
     post_id: "post_id_0"
     uid: "uid_1"
     likes: "10"
   comment_1
     post_id: "post_id_0"
     uid: "uid_1"
     likes: "7"
   comment_2
     post_id: "post_id_1"
     uid: "uid_1"
     likes: "2"

The setup:

class CommentClass {
    var commentKey = ""
    var comment = ""
    var likes = ""
}

var postsArray = [ [String: [String:AnyObject] ] ]()
var commentsArray = [CommentClass]()

The code to load all of the posts:

    let postsRef = ref.child("posts")

    postsRef.observeSingleEvent(of: .value, with: { snapshot in

        for snap in snapshot.children {
            let postSnap = snap as! FIRDataSnapshot
            let postKey = postSnap.key //the key of each post
            let postDict = postSnap.value as! [String:AnyObject] //post child data

            let d = [postKey: postDict]
            self.postsArray.append(d)
        }
        //postsTableView.reloadData
        print(self.postsArray) //just to show they are loaded
    })

then, when a user taps a post, load and display the comments.

    self.commentsArray = [] //start with a fresh array since we tapped a post
    //placeholder, this will be the post id of the tapped post
    let postKey = "post_id_0" 
    let commentsRef = ref.child("comments")
    let queryRef = commentsRef.queryOrdered(byChild: "post_id")
                              .queryEqual(toValue: postKey)

    //get all of the comments tied to this post
    queryRef.observeSingleEvent(of: .value, with: { snapshot in

        for snap in snapshot.children {
            let commentSnap = snap as! FIRDataSnapshot
            let commentKey = commentSnap.key //the key of each comment
            //the child data in each comment
            let commentDict = commentSnap.value as! [String:AnyObject] 
            let comment = commentDict["comment"] as! String
            let likes = commentDict["likes"] as! String
            let c = CommentClass()
            c.commentKey = commentKey
            c.comment = comment
            c.likes = likes

            self.commentsArray.append(c)
        }

        //commentsTableView.reload data

        //just some code to show the posts are loaded
        print("post:  \(postKey)")
        for aComment in self.commentsArray {
            let comment = aComment.comment
            print("  comment: \(comment)")
        }
    })

and the resultant output

post:  post_id_0
  comment: I like post_id_0
  comment: post_id_0 is the best evah

The above code is tested and has no flicker. Obviously it will need to be tweaked for your use case as you have some images to load etc but the above should resolve issues and be more maintainable.

Jay
  • 34,438
  • 18
  • 52
  • 81
  • Thank you so much. Thank you for your time. I think this is the best approach. This is actually how my data was structured in one iteration of the project, so it was a simple fix to the observer code which fixed the problem. – jasonhdev Jan 15 '17 at 16:06
  • One quick questions, I don't know how appropriate it is in SO's q/a format, but is there any kind of cost associated with doing it this way? Is it ever bad to query? I read somewhere that it was a costly thing to do, and that's why I was trying to avoid it. – jasonhdev Jan 15 '17 at 16:12
  • @jasonhdev If the answer helped, please accept it! There are no additional costs involved, and it's never 'bad' to query. However, query's have more overhead than observes so if you can observe instead of query it's more efficient app-wise. It really comes down to: do you want *all* the data in a node or a *selection* of data in a node. Selection is less data, therefore more efficient and less bytes downloading. The Firebase plan costs are based on what how much you are storing and how much you download per month. The Free Spark plan is 10G a month down and 1G storage; plenty for development. – Jay Jan 15 '17 at 16:28
  • Okay. Sorry for the delay in accepting the answer. I didn't know I was supposed to accept an answer. For the records, I had previously tried to click the up arrow icon thingy, but it didn't let me. – jasonhdev Jan 19 '17 at 22:41
  • @jasonhdev I am so glad it helped! It can be pretty daunting when you are first laying out a project so keep trucking and yes, you did it right! – Jay Jan 19 '17 at 22:43
0

I experienced this flickering only with the pictures. The app downloaded the pictures every time when something happened inside the collection view.

My solution what I have found on the web. NSCache. You can Cache the image, and if the image link did not change the picture will be loaded from cache. Also helpful when you scrolling a collectionview like Instagram or Facebook. I did not find too many solution for this in Swift 5. So let me share with you.

TODO: Create a new swift file. Copy this code, it creates a custom ImageView class. Set up you imageview for this custom class on storyboard, ctrl+drag. Or do the same programatically in you swift file.

let imageCache = NSCache<NSString, UIImage>()
class CustomImageView: UIImageView {
var imageUrlString: String?

func loadImageUsingUrlString(urlString: String) {
    
    imageUrlString = urlString
    
    guard let url = URL(string: urlString) else { return }
    
    image = nil
    
    if let imageFromCache = imageCache.object(forKey: urlString as NSString) {
        self.image = imageFromCache
        print("local")
        return
    }
    
    URLSession.shared.dataTask(with: url, completionHandler: { (data, respones, error) in
        
        if error != nil {
            print(error ?? "")
            return
        }
        
        DispatchQueue.main.async {
            guard let imageToCache = UIImage(data: data!) else { return }
            
            if self.imageUrlString == urlString {
                self.image = imageToCache
                print("most mentem a kepet")
            }
            
            imageCache.setObject(imageToCache, forKey: urlString as NSString)
        }
        
    }).resume()
}
}