Swift user online status updates but it repeats on cells that is not supposed to

I'm working on a chat app and I configured a function to check if a user is online. I'm able to see if another user is active or not, but, the issue I'm having is that if I scroll down (I'm using a UITableView) other users show as active and they are not. I placed the code inside the UITableViewCell class. Any suggestions as to what could be the problem are greatly appreciated. Here is my code:

UITableViewCell

`func configureHomeFeedCell(member: Member) {
    profileImage.loadImage(with: member.imageURL)
    profileName.text = "\(member.name)" + ", " + "\(member.age)"
    
    checkUserOnlineStatus(with: member.documentId) { _ in }
    
}

func checkUserOnlineStatus(with userId: String, completion: @escaping(Bool) -> Void) {
    let query = USERS_COLLECTION.document(userId).collection(IS_ONLINE)
    query.getDocuments { (snapshot, error) in
        if let error = error {
            print("ERROR..\(error.localizedDescription)")
        } else {
            snapshot?.documents.forEach({ diff in
                let isOnline = diff.get(USER_IS_ONLINE) as? Bool
                self.onlineViewStatus.backgroundColor = isOnline == true ? .green : .red
                completion(isOnline!)
            })}}
        query.addSnapshotListener { (snapshot, error) in
            snapshot?.documentChanges.forEach { diff in
                let isOnline = diff.document.get(USER_IS_ONLINE) as? Bool
                if (diff.type == .modified) {
                    self.onlineViewStatus.backgroundColor = isOnline == true ? .green : .red
                    completion(isOnline!)
                }}}
}`

Replies

I suspect a race condition due to using async operations to set up the cell. If getDocuments() is async, then the completion handler may be called after the cell has already appeared on screen, scrolled away, and even been reused for a different member that has scrolled onscreen. If that happens, the completion handler’s capture of self refers to the reused cell, which no longer represents the original member. (And the snapshot listener could cause an extreme case of this, if a snapshot update could happen a long time after you create it.)

You could try to work around this by saving the Member or its documentId in each cell and checking it in the completion handler to make sure it matches, but it’s still not a great solution. This cell setup code will get called a lot as you scroll and it’s best to keep it fast and avoid redundant work. Can you do all this async work in your view controller or view model before you show the table? And then update the table when you receive changes.

  • thank you for your reply Scoot, yes, i was thinking on moving the checkUserOnlineStatus function to the view controller. I'll have to create a delegate, since the onlineViewStatus UIView is part of the cell. I'll give it a try and will update my code here. thank you again.

Add a Comment

Thank you Scott, yes, I was thinking on moving the function to the view controller, but, i need to add a delegate for the onlineViewStatus UIView as this is inside the cell.

What would such a delegate do? Your onlineViewStatus view (btw, would the name be more clear as onlineStatusView?) seems to be just an “output” via its background color, and doesn’t seem to have any actual behavior that would need to be reported via a delegate back to the view controller. Is this correct?

Try to make the cell behavior as simple as possible: you have an image, a name, and a view that shows online status. You already get the image and name from the Member object which is passed to the cell during configuration. Similarly, now you’ll want to pass the online status to the cell when you receive it, and then the cell just sets the color. Then the cell class becomes extremely simple.

The fun starts at “pass the online status to the cell when you receive it.” So the view controller would launch async operation(s) to fetch everyone’s online status, and then updates the cell for each member when that member’s online status is received. The best way to do this is to build your table using a diffable data source. (If you’re not already using this, I strongly recommend it. It makes things much easier.) Then you use the diffable data source snapshot to tell the table to redraw the cells that need updating.

Thank you for the last suggestion, I ended moving the function from the cell class to the view controller and called it inside cellForRowAt, so far, it's working as expected. Here is the updated code:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let hCell = homeFeedTableView.dequeueReusableCell(withIdentifier: "HomeFeedCell", for: indexPath) as! HomeFeedCell
     
    hCell.selectionStyle = .none
    hCell.backgroundColor = UIColor.white
     
    if !members.isEmpty {
      hCell.configureHomeFeedCell(member: self.members[indexPath.row])
      checkUserOnlineStatus(with: members[indexPath.row].documentId) { isOnline in
        hCell.onlineViewStatus.backgroundColor = isOnline == true ? .green : .red
      }
    }
    return hCell
  }
   
   
  func checkUserOnlineStatus(with userId: String, completion: @escaping(Bool) -> Void) {
    let query = USERS_COLLECTION.document(userId).collection(IS_ONLINE)
    query.getDocuments { (snapshot, error) in
      if let error = error {
        print("ERROR..\(error.localizedDescription)")
      } else {
        snapshot?.documents.forEach({ diff in
          let isOnline = diff.get(USER_IS_ONLINE) as? Bool
          completion(isOnline!)
        })}}
      query.addSnapshotListener { (snapshot, error) in
        snapshot?.documentChanges.forEach { diff in
          let isOnline = diff.document.get(USER_IS_ONLINE) as? Bool
          if (diff.type == .modified) {
            completion(isOnline!)
          }}}
  }

Glad it’s working, but this still seems vulnerable to the original problem. The completion handler still captures and updates the cell you are setting up (hCell) but if the handler actually runs after any appreciable time delay, then the captured hCell may not refer to the cell you want.

Also, what’s up with the members.isEmpty check? Sounds like members is your data model, so if it’s empty, then why are there any cells being displayed at all?

Or if members being empty means you are displaying a placeholder cell (e.g. “Sorry, you have no friends.”) then maybe you want to check earlier and dequeue a cell with a different identifier that is specially designed for this case.

OK, I had to think about this one more time and i finally got it working 100%. Aside from the call to get all the user documents, I was making another call at the same time to the collection that was storing the values for isOnline. I added isOnline to the members model and a separate snapshot listener to monitor the changes to that key. Now everything is working 100%. Hope this helps anyone.

I have updated my code:

Service Class

**This function is to update the user's presence when online or not and it's called inside SceneDelegate **

 static func isUserOnline(bool: Bool) {
    guard let currentUID = Auth.auth().currentUser?.uid else { return }
     
    if !currentUID.isEmpty {
      let dictionary = [USER_IS_ONLINE: bool as Any,
             USER_LASTEST_ONLINE: Date().timeIntervalSince1970 as Any] 
       
      USERS_COLLECTION.document(currentUID).getDocument { (document, error) in
        if let error = error {
          print("Error..\(error.localizedDescription)")
        } else {
          if document?.exists == true {
            USERS_COLLECTION.document(currentUID).updateData(dictionary)
          } else {
            USERS_COLLECTION.document(currentUID).updateData(dictionary)
          }}}}}

**SceneDelegate**

func sceneDidDisconnect(_ scene: UIScene) {
    Service.isUserOnline(bool: false)
  }

  func sceneDidBecomeActive(_ scene: UIScene) {
    Service.isUserOnline(bool: true)
  }

  func sceneWillResignActive(_ scene: UIScene) {
    Service.isUserOnline(bool: false)
  }

  func sceneDidEnterBackground(_ scene: UIScene) {
    Service.isUserOnline(bool: false)
  }


**This function is to monitor the user activity**
   
  static func checkUserOnlineStatus(with userId: String, completion: @escaping(Bool) -> Void) {
    let query = USERS_COLLECTION.document(userId)
     
    query.getDocument { (document, error) in
      if let document = document, document.exists {
        let isOnline = document.get(USER_IS_ONLINE) as? Bool ?? false
        completion(isOnline)
      }
    }
    query.addSnapshotListener { (document, error) in
      if let document = document, document.exists {
        let isOnline = document.get(USER_IS_ONLINE) as? Bool ?? false
        completion(isOnline)
      }}}

Calling the function inside the cell class

func configureHomeFeedCell(member: Member) {

 Service.checkUserOnlineStatus(with: member.documentId) { isOnline in
      self.onlineViewStatus.backgroundColor = isOnline == true ? .green : .red
    } }