1

I am making a social app to which I am fetching some data and flushing it to the collection view. I am flushing the all the posts from firebase to the posts array. I am also fetching the user information that posted the specific image. Both the database are 2 different models. Following is my data model :

posts
  |- <post_id>
         |- caption
         |- ImageURL
         |- views
         |- spot
             |- spot_id
                  |- sender<user_id>
                  |- spotted(value)
                  |- timestamp
         |- author(<user_id>)

users
  |- <user_id>
         |- name

Following is the way I am fetching the post data in collectionVC and storing all to posts array:

func initialiseAllPostsContent(){
    FBDataservice.ds.REF_CURR_USER.child("connections/following").observe(.childAdded) { (snapshot) in
        if let snapshot = snapshot.value as? String {
            self.followerKeys.append(snapshot)
        }
    }
    if uid != nil {
        self.followerKeys.append(uid!)
    }
    FBDataservice.ds.REF_POSTS.queryOrdered(byChild: "timestamp").observe(.childAdded, with: { (snapshot) in
        print("post key is ", snapshot.key)
        if let postDict = snapshot.value as? Dictionary<String, Any> {
            let key = snapshot.key
            if let postAuthor = postDict["author"] as? String {
                for user in self.followerKeys {
                    if postAuthor == user {
                        let post = Posts(postId: key, postData: postDict)
                        self.posts.append(post)
                    }
                }
            }
        }
    })
    reloadCollectionViewData()
}

func reloadCollectionViewData() {
    FBDataservice.ds.REF_POSTS.queryOrdered(byChild: "timestamp").observe(.value) { (snapshot) in
        self.collectionView.reloadData()
    }
}

//I am updating the views on the post after a method is successfull. As soon as this is called, and then if like is pressed, views flicker
func updateViews(postid: String, views: Int) {
    let viewref = FBDataservice.ds.REF_POSTS.child(postid)
    let newviews = views + 1
    viewref.updateChildValues(["views":newviews])
}

// fetching the user data from the post data

func getAllPosts(pid: String, completion: @escaping ((String) -> ())) {
    FBDataservice.ds.REF_POSTS.child(pid).observeSingleEvent(of: .value) { (snapshot) in
        if let snapshot = snapshot.value as? Dictionary<String, Any> {
            if let userid = snapshot["author"] as? String {
                completion(userid)
            }
        }
    }
}

func getpostAuthorData(authorId : String, completion: @escaping (User) -> ()) {
    FBDataservice.ds.REF_USERS.child(authorId).observeSingleEvent(of: .value) { (snapshot) in
        if let snapshot = snapshot.value as? Dictionary<String, Any> {
            if let userCredential = snapshot["credentials"] as? Dictionary<String, Any> {
                completion(User(userid: authorId, userData: userCredential))
            }
        }
    }
}

This is how I am assigning data in my cellForItemAtIndexPath

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    self.posts.sort(by: { $0.timestamp < $1.timestamp})
    let post = posts[indexPath.row]
    if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as? SpotGroundCell {
        cell.configureCellData(post: post)
        getAllPosts(pid: post.postId) { (userid) in
            self.getpostAuthorData(authorId: userid, completion: { (userdata) in
                cell.configUserData(user: userdata)
            })
        }
    return cell
    } else {
        return SpotGroundCell()
    }

}

The code in my cell :

//Consider this as likes. I allow users to like multiple times. Once the model is loaded, it fetches all the spots according to the timestamp and then siplayer the most recent ones. Even this is doesn't display according to the current image and flickers. I replicate previous cell values even though I am refreshing the view.
var currentUserSpots = [Spot]() {
    didSet {
        self.currentUserSpots.sort(by: { $0.timestamp < $1.timestamp})
        if !self.currentUserSpots.isEmpty {
            self.emotionImage.image = UIImage(named: (self.currentUserSpots.first?.spotted)!)
            self.emotionImage.alpha = 1
        } else {
            self.emotionImage.image = UIImage(named: "none")
            self.emotionImage.alpha = 0.5
        }
    }
}

func configUserData(user: User) {
    self.user = user
    self.name.text = self.user.name
}

func configureCellData(post: Posts) {
    print("Config is now called")
    self.posts = post
    self.caption.text = posts.caption

    FBDataservice.ds.REF_POSTS.child(post.postId).child("spot").queryOrdered(byChild: "senderID").queryEqual(toValue: uid!).observeSingleEvent(of: .childAdded) { (snapshot) in
        if let spotData = snapshot.value as? Dictionary<String, Any> {
            let spot = Spot(id: snapshot.key, spotData: spotData)
            if spot.spotted != nil {
                self.currentUserSpots.append(spot)
            }
        }
    }
}

Now whenever I am making a change or an event which updates the database(like updating a view). I see a flicker in the user object entities(such as name etc). That event also kills other processes and Notification Observers.

I scrapped the internet for the solutions, but by far just was able to find one, which doesn't solve my problem.

Any help will be greatly appreciated. I am really not sure where am I going wrong.

Aakash Dave
  • 866
  • 1
  • 15
  • 30

1 Answers1

1

Whenever there is a change under REF_POSTS you right now:

  1. delete all data from the view
  2. re-add all data (including the change) to the view

Given that most changes will only affect one item in the list, you're making your view to N-1 more than is needed. This causes the flicker.

To solve this problem, you should listen to more granular information from the database. Instead of observing .value, add a listener for .childAdded. The completion block for this listener will be triggered whenever a new child is added, at which point you can just add the new child to your view.

FBDataservice.ds.REF_POSTS.queryOrdered(byChild: "timestamp").observe(.childAdded, with: { (snap) in
    if let postDict = snap.value as? Dictionary<String, Any> {
        let key = snap.key
        if let postAuthor = postDict["author"] as? String {
            for user in self.followerKeys {
                if postAuthor == user {
                    let post = Posts(postId: key, postData: postDict)
                    self.posts.append(post)
                }
            }
        }
    }
})

As a bonus .childAdded also immediately fires for all existing child nodes, so you don't need the observer for .value anymore. I like keeping it myself though. As Firebase guarantees that it fires .value after all corresponding child* events, the .value event is a great moment to tell the view that all changes came in.

FBDataservice.ds.REF_POSTS.queryOrdered(byChild: "timestamp").observe(.value, with: { (snapshot) in
    self.collectionView.reloadData()
})

You'll need a few more things for a complete implementation:

  1. You should also observe .childChanged, .childMoved and childRemoved to handle those types of changes to the database.
  2. Since a child may be added (or moved) anywhere in the list, you should actually use observe(_, andPreviousSiblingKey: ) to be able to put the item in the right spot in the list.
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Please correct me if I am wrong, `.childAdded` will only be triggered when there is an addition. Will this be okay to init all the data on loading the view. I have also made an edit in the code above. I am first fetching all the uids of people who i am following and then am getting all the values. Here I am technically having 2 observers. – Aakash Dave Nov 07 '18 at 02:23
  • As said "As a bonus `.childAdded` also immediately fires for all existing child nodes". – Frank van Puffelen Nov 07 '18 at 03:10
  • As per the docs `The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot and the previous child’s key.` Why are we given the previous child's key. Is this the key which will be observed for the updates? – Aakash Dave Nov 07 '18 at 22:27
  • You're given the previous child's key, since (after the initial load) a child may be added (or moved) anywhere in the list. – Frank van Puffelen Nov 08 '18 at 00:12
  • I am sorry Puff, I am asking recurrently, But how would be supplying the key for the previous child help. Lets say, a user comments on or liked the post, then if I use the above solution, I am not able to get realtime updated count. Plus I am still having the flickering issue(I havent used the `observe(_, andPreviousSiblingKey: )` yet). I am losing you somewhere. please point me to the right direction. Your help is very much appreciated. – Aakash Dave Nov 08 '18 at 04:28
  • Should I be using something such as `unTransactionBlock` for the realtime count update? – Aakash Dave Nov 08 '18 at 07:25
  • If you've used `.childAdded` and still have flicker, update your question to show what you've done. – Frank van Puffelen Nov 08 '18 at 13:59
  • Have you had a change to see this? – Aakash Dave Nov 09 '18 at 23:55
  • I did, but there's just too much code with too many things going on. The basic approach to reduce the flicker is as I outlined in my answer: listen to child-level events for data changes, and then listen to `value` to tell the view of those updates. – Frank van Puffelen Nov 09 '18 at 23:59
  • Alright. So, for events like increasing views or likes, I should have separate observers and then a `.value` observer to update the overall datasource? – Aakash Dave Nov 10 '18 at 00:02