-1

When I load my data from an ordered array, the table view goes through every cell when loading. It seems as if the table view loads first and then the array is ordered, which leads to the flickering effect. This, in turn, also messes up the images that each cell has.

This is called after the snapshot gets the data

     postReference.observeSingleEvent(of: .value, with: { (snapshot) in

                if let value = snapshot.value as? NSDictionary {
                let post = Post()
                let post_content = value["post"] as? String ?? "Content not found"

                post.revealDate = value["revealedDate"] as? Double ??  0000000         
                            self.postList.append(post)
                    }

                    self.postList.sort() { $0.revealDate! > $1.revealDate! }
                    self.tableView.reloadData()

            }

and this loads the cell

public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if queryComplete == true{


 let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! PostTableViewCell
            //set cell content
            let contentOfCellPost = postList[indexPath.row]
            cell.label?.text = contentOfCellPost.post_words
            return cell

This is how the posts flicker when the table view loads

It seems as if the table view is loading each cell as the first cell then ordering it afterwards.

EDIT

Entire firebase initial data load observation:

func fetchFeed () {
    let UID = Auth.auth().currentUser?.uid
    feedReference = Database.database().reference().child("feeds").child(UID!)
    feedReference.keepSynced(true)

    self.postRefHandle = feedReference.observe(.childAdded) { (snapshot) in
       let postID = snapshot.key
       let postReference = Database.database().reference().child("posts").child(postID)

       postReference.observeSingleEvent(of: .value, with: { (snapshot) in

           if let value = snapshot.value as? NSDictionary {
               let post = Post()
               let post_content = value["post"] as? String ?? "Content not found"
               post.Reveals = value["reveals"] as? Int ?? 10000000
               post.revealsRequired = value["revealsRequired"] as? Int ?? 100000
               post.post_words = post_content
               post.postID = postID
               post.Revealed = value["Revealed"] as? String ?? "Revealed or Not not found"
               post.revealsRequired = value["revealsRequired"] as? Int ?? 1000000000
               post.revealDate = value["revealedDate"] as? Double ??  0000000
               post.timeOfDeletion = value["timeOfDeletion"] as? Int ?? 100000000000

               if snapshot.hasChild("information") {
                   if let infoName = snapshot.childSnapshot(forPath: "information").value as? NSDictionary {
                       post.name = infoName["posterName"] as? String ?? "Poster not found"
                       post.profileImageLink = infoName["profileImage"] as? String ?? "nil"
                       post.revealedBool = true
                       self.postList.append(post)
                   }
               } else {
                  let date = Date()
                  let currentTime = date.timeIntervalSince1970 * 1000
                  let timeToDelete = Int(post.timeOfDeletion!) - Int(currentTime)
                  post.revealedBool = false
                  post.name = String(timeToDelete)
                  self.postList.append(post)
              }

              self.postList.sort() { $0.revealDate! > $1.revealDate! }
              self.tableView.reloadData()
        }
    })
  }
}

Observing for changes in the post :

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



                if self.queryComplete == true {
                    let changedKey = snapshot.key
                    if let index = self.postList.index(where: {$0.postID == changedKey}) {
                        let indexPath = IndexPath(row: index, section: 0)
                        let changedPost = self.postList[index]
                        if let value = snapshot.value as? NSDictionary {

                            let newReveals = value["reveals"]  as? Int
                            changedPost.Reveals = newReveals
                            //update the values
                            self.tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.none)
                        }
                        if let value = snapshot.value as? NSDictionary {
                            let newRevealStatus = value["Revealed"]  as? String ?? "noRevealStatusChange"
                            changedPost.Revealed = newRevealStatus

                            if changedPost.Revealed == "true" {
                                changedPost.revealedBool = true
                                changedPost.revealDate = value["revealedDate"] as? Double ?? 00000


                                if let newName = snapshot.childSnapshot(forPath: "information").value as? NSDictionary {

                                    changedPost.name = newName["posterName"] as? String ?? "Poster not found"
                                    changedPost.profileImageLink = newName["profileImage"] as? String ?? "nil"
                                    self.postList.sort() { $0.revealDate! > $1.revealDate! }

                                    self.tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.none)
                                }


                            }

                        }
                    }
                }

                 self.queryComplete = true
        } )

JSON sample :

"feeds" : {
    "3ASP4M5mkTPGPO1qQhfVKXsr6Qf2" : {
        "-L65VFpW2cYIMxG2e9Ll" : "true",
        "-L65VH5jkKPYAguzgqpn" : "true"
    }
"posts" : {
    "-L7CpeKT2lzsPALAfNti" : {
        "Revealed" : "true",
        "datePosted" : "2018-03-10 02:56:33 +0000",
        "information" : {
            "posterID" : "BmVot3XHEpYwMNtiucWSb8XPPM42",
            "posterName" : "tester",
            "profileImage" : "nil"
        },

EDIT : Modified Code with less observers :

func fetchFeed () {

    let UID = Auth.auth().currentUser?.uid
    feedReference = Database.database().reference().child("feeds").child(UID!)
    feedReference.keepSynced(true)

    self.postRefHandle = feedReference.observe(.childAdded) { (snapshot) in
    let postID = snapshot.key
    let postReference = Database.database().reference().child("posts").child(postID)
        postReference.observe(.value, with: { (snapshot) in
            let postKey = snapshot.key
             if let value = snapshot.value as? NSDictionary {
            if let index = self.postList.index(where: {$0.postID == postKey}) {
                let indexPath = IndexPath(row: index, section: 0)
                let changedPost = self.postList[index]


                    let newReveals = value["reveals"]  as? Int
                    changedPost.Reveals = newReveals
                    //update the values
                    self.tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.none)

                    let newRevealStatus = value["Revealed"]  as? String ?? "noRevealStatusChange"
                    changedPost.Revealed = newRevealStatus

                    if changedPost.Revealed == "true" {
                        changedPost.revealedBool = true
                        changedPost.revealDate = value["revealedDate"] as? Double ?? 00000


                        if let newName = snapshot.childSnapshot(forPath: "information").value as? NSDictionary {

                            changedPost.name = newName["posterName"] as? String ?? "Poster not found"
                            changedPost.profileImageLink = newName["profileImage"] as? String ?? "nil"
                            self.postList.sort() { $0.revealDate! > $1.revealDate! }


                            self.roundButton.isHidden = false
                        }


                    }

                  self.tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.none)
            }

                //appending to row for first time (initial load)
            else {
                if let value = snapshot.value as? NSDictionary {
                    let post = Post()
                    let post_content = value["post"] as? String ?? "Content not found"
                    post.Reveals = value["reveals"] as? Int ?? 10000000
                    post.revealsRequired = value["revealsRequired"] as? Int ?? 100000
                    post.post_words = post_content
                    post.postID = postID
                    post.Revealed = value["Revealed"] as? String ?? "Revealed or Not not found"
                    post.revealsRequired = value["revealsRequired"] as? Int ?? 1000000000
                    post.revealDate = value["revealedDate"] as? Double ??  0000000
                    post.timeOfDeletion = value["timeOfDeletion"] as? Int ?? 100000000000

                    if snapshot.hasChild("information"){

                        if let infoName = snapshot.childSnapshot(forPath: "information").value as? NSDictionary {

                            post.name = infoName["posterName"] as? String ?? "Poster not found"
                            post.profileImageLink = infoName["profileImage"] as? String ?? "nil"
                            post.revealedBool = true
                            self.postList.append(post)


                        }
                    }
                    else  {
                        let date = Date()
                        let currentTime = date.timeIntervalSince1970 * 1000
                        let timeToDelete = Int(post.timeOfDeletion!) - Int(currentTime)
                        post.revealedBool = false
                        post.name = String(timeToDelete)

                        self.postList.append(post)

                    }
                    // self.deletePosts()

                    self.postList.sort() { $0.revealDate! > $1.revealDate! }

                    self.tableView.reloadData()



                }

            }
            }
             else {
               Database.database().reference().child("feeds").child(self.userIDCurrent!).child(postID).removeValue()
            }
        })
    }
    self.queryComplete = true
}
Raim Khalil
  • 387
  • 3
  • 19
  • https://stackoverflow.com/questions/44734308/whats-the-difference-between-tableview-reloaddata-and-tableview-reloadrows – Code Different Jun 11 '18 at 21:19
  • @Code Different, how can I reload a specific row before the cell has even been added? How would I get the index path? – Raim Khalil Jun 11 '18 at 21:22
  • That code looks a little odd for observing a node by .value. Can you include a snippet of your Firebase structure as text in your question so we can take a look? – Jay Jun 11 '18 at 21:28
  • @Jay, I updated the code. The json structure uses a fanning out system such that the post ID's are in each feed. When observing the data, I initially collect every post in the feed then create a separate reference to that for the initial load. Then, I do a reference for each post that just observes for changes, but I did not include the code for brevity. – Raim Khalil Jun 11 '18 at 21:35
  • @Jay I added the observation for changes in the post also. – Raim Khalil Jun 11 '18 at 21:52
  • 1
    The sequence of events is a little unclear but one thing that jumps out at me is (and correct me if I am misunderstanding) that you're using *postReference.observeSingleEvent* to populate the dataSource array but then at some point this happens *postReference.observe(.value* which will essentially reload that same data but leave an observer. The first observe updates the entire tableView and the second updates those same rows again. It appears all of that could be combined into a single observer call to both populate the posts array and leave the observer watching those posts for changes. – Jay Jun 11 '18 at 22:40
  • @Jay, this is correct, however I need these two observations because if I just have the first one, the observe single event, it won’t observe changes in the post. If I just have the observe value, it won’t fetch the original posts and add them to the array. If I make the observation of the value add to an array, then every time a post is changed a new cell will be added. I think the flickering effect is because of the ordering of the array, because when I remove the ordering the table view loads correctly but in the wrong order. – Raim Khalil Jun 11 '18 at 23:12
  • @Jay, would it be more efficient to have only one observer, then when appending the post check if the post ID exists in the array and if it does, simply modify the existing value in the array? – Raim Khalil Jun 11 '18 at 23:16
  • Okay, moved everything into one observation - flickering problem still exists. – Raim Khalil Jun 12 '18 at 00:02
  • Yes - one observe would handle it but in some cases you should use .childAdded, .childChanged, .childRemoved to monitor the child nodes for changes - it's difficult without knowing the scope of the project. Since the flickering still exists, obviously there's a coding issue so we would need to see updated code. Also, as least with the old code, it appears you're adding an observer to every post child node? I am all for denormalizing data but can you just have a *for_feed: feed_id* child node within posts and add a query observer for that node? It would be a lot less code. – Jay Jun 12 '18 at 12:12
  • @Jay, I added the modified code. I understand that it may be demanding to add an observe to every single post, but it is necessary - the feeds section only contains the post ID. I can't create a copy of every post in every feed, as a person may have thousands of friends and likes, dates, and content in the post are constantly changing so even firebase cloud functions would be inadequate to update every copy. In the new code, there is some observation for observing whether the post still exists; this is merely to make sure the feed section does not have too much dead data. Problem still exists. – Raim Khalil Jun 12 '18 at 20:34
  • I'll address the flickering issue separately. How about a different observer approach? Suppose you add a child node to each post called /watched_by (e.g. /posts/post_id/watched_by) and within watched_by add child nodes of the users uid who wants to be notified of any changes to that node and set to true (/watchedBy/uid_x: true). Use a .childChanged deep query on the posts node of: /watched_by/uid = true. From then on any time a post is changed that is being watched by that user, they will be notified. That eliminates the need for thousands of observers. If interested in the code, let me know. – Jay Jun 12 '18 at 21:03
  • @Jay that seems really interesting! Would that be faster/ more efficient? That makes sense to me - would it completely remove the need for feeds altogether? Also, how would the client side code look like - would it not be massive with millions of posts, as the user would have to go through every single post one by one in the database and see if he or she belongs to that post's watchedBy child, then observe for changes? – Raim Khalil Jun 12 '18 at 21:14
  • I may allows feeds to be removed and it would be potentially more efficient. When a user authenticates, capture their uid and drop a query on the posts node, dates node etc. and that user will be notified of any changes that apply to them within that node. I am totally guessing that it will help and doesn't directly have anything to do with the flickering issue (I am still working on that one) but it may make for a lot cleaner and reduced code base. – Jay Jun 12 '18 at 21:24
  • @Jay 'ref.orderByChild("members/userID").equalTo(true).observe(.childChanged);' so something like this, that observes for child changed for every post the user is a member of, would be faster than feeds? It seems like a more elegant solution, as it also removes the need for the user to delete the post from their feed when it is deleted. One tangential question, I am running a cron job on all the posts, too, that checks if the timestamp is too old. This cron job runs every minute; with hundreds of thousands of posts, would it get too massive? – Raim Khalil Jun 12 '18 at 21:33

1 Answers1

1

I going to take a shot at this and approach the issue by suggesting an alternative direction. If this is far off base, let me know and I can update or delete.

There's a lot of code in the question to sort through but on the surface it appears there's a whole lot of observers being added, reloading of data and refreshing the tableView frequently.

From what I can see, the feeds node is being used to gather together groups of data; a group of posts the user is interested in, a group of dates, a group of friends etc. each one being a feed and every node connected to that feed has an observer added.

My suggestion is to take go at it at a higher level - instead of addressing each child node with a separate, independent observer, let's say 'Hey Firebase, within this parent node, if a child node meets a certain criteria, let the user know about it'

So let's leverage Firebase deep queries to do the same thing with far fewer observers.

Take an example posts structure

posts
   post_0
     name: "Jim"
     post: "We have them just where they want us"
     watched_by:
        uid_0: true
        uid_1: true
   post_1
     name: "Spock"
     post: "Nothing unreal exists"
     watched_by:
        uid_1: true
   post_2
     name: "Bones"
     post: "I'm a doctor, not an escalator"
     watched_by
        uid_0: true

and assume that user with uid_0 authenticates. Previously, they decided to watch posts by Jim and Bones. As you can see from the structure we've got a watched_by node within each post and have added uid_0: true to that node.

So, let's now let uid_0 observe the posts node for changes to the posts they are interested in:

let postsRef = self.ref.child("posts")
let watchedByQuery = postsRef.queryOrdered(byChild: "/watched_by/uid_0").queryEqual(toValue: true)
watchedByQuery.observe(.childChanged, with: { snapshot in
    let dict = snapshot.value as! [String: Any]
    let post = dict["post"] as! String
    print(snapshot.key, post)
})

so if Bones logs in and changes his post from "I'm a doctor, not an escalator" to "I'm a doctor, not a bricklayer" that node will be sent to uid_0 so the ui can be updated.

   post_2
     name: "Bones"
     post: "I'm a doctor, not a bricklayer"
     watched_by
        uid_0: true

expanding on this, we would obviously add .childAdded and a .childRemoved using the same technique and the .childAdded would populate our tableView dataSource initially for each user as they authenticate.

Suppose Bones then totally deletes his post (node). Any user in the watched_by list would be notified of that event (even millions of users). When that happens, in code, read the key of post_2 from the snapshot, find it in your datasource array, remove it and update your tableView.

Again - a total shot in the dark answer on my part.

Jay
  • 34,438
  • 18
  • 52
  • 81
  • I am facing a bit of dilemma. The structure you have provided is way more efficient, reduces observers, and prevents the flickering. However, the post sends all the watched_by data to every user who is accessing it, and the entire idea is supposed to be about anonymous posting. Thus, when a user accesses a post, although the UI does not display it, all the data (including who the user's friends were) will be sent to every feed... is this a serious problem? Any suggestions on how to avoid this? – Raim Khalil Jun 13 '18 at 17:50
  • @RaimKhalil UID's are very generic and don't contain any direct user information. So even though the app receives a bunch of uid's there no backtracking to see who that is. More importantly, a user would have to be debugging your source code and then inspect that snapshot to see what the uid's are in the first place. If it makes you more comfortable, don't use a UID, use some other randomly generated string or UUID and store it in each users /users node like */users/uid_x/my_random_code: xyzzy* and then use that code in place of uid's to observe for. – Jay Jun 13 '18 at 18:30
  • 1
    @RaimKhalil Here's a link from a Firebaser's answer that speaks to UID security [UID](https://stackoverflow.com/questions/42620723/firebase-database-risks-associated-with-exposing-uid-on-the-client-side) – Jay Jun 13 '18 at 18:38
  • that makes a lot of sense! Appreciate all the help from the last few days, I think UID’s are fine, then, in terms of security. – Raim Khalil Jun 13 '18 at 22:44
  • I know it’s been a while, but a related question about querying. When I query all the children in the posts that have a watchedBy containing the user, won’t the client have to download every single post to query? If there are hundreds of thousands of posts, won’t the client have to download each and every one in order to query where the watchedBy contains the userID? – Raim Khalil Jun 25 '18 at 20:30
  • 1
    @RaimKhalil Query's are done on the server side and the only data sent to your app is the data that matches the query. In other words if you add a childAdded query to a node for */watched_by/uid_0* the only nodes that will be fed to your app are those when added, that match that. Others are ignored. – Jay Jun 25 '18 at 20:34
  • one more thing, for this answer with the watchedBy node, how would I index it in the database rules? The query is based off the userID, so there is no way to index based off that? – Raim Khalil Jun 28 '18 at 20:24