Search code examples
iosswiftfirebasefirebase-realtime-databaseuser-experience

Like feedback using firebase crash. Fast click like-remove like


I have an application like instagram. It has feedback page.

When user likes some post, I add this like and feedback (with its own key (.childByAutoId) for this like.

static func add(_ newLike: LikeItem) {
  // add like id for user feedback implementation
  var like = newLike
  let likeRef = ref.child("/userslikes/" + newLike.userId + "/onposts/" + newLike.postId).childByAutoId()
  like.key = likeRef.key

  var updates: [String: Any?] = [
     "/userslikes/" + like.userId + "/onposts/" + like.postId: like.toAnyObject(),
     "/postslikes/" + like.postId + "/"         + like.userId: like.toAnyObject()
  ]

  if like.userId != like.postAddedByUserId { // dont add your own likes
     var likeForFeedBack = like.toAnyObject()
     likeForFeedBack["isViewed"] = false // when user will open feedback -> true
     updates.updateValue(likeForFeedBack, forKey: "/feedback/" + like.postAddedByUserId + "/" + like.key)
  }

  ref.updateChildValues(updates)
}

It's ok. And also I have remove function. It is going to like node, getting this like and feedbackId from this like. And then I make multi-part update.

static func remove(with userId: String, _ post: PostItem) {
  var updates: [String: Any?] = [
     "/userslikes/" + userId   + "/onposts/" + post.key: nil,
     "/postslikes/" + post.key + "/"         + userId:   nil
  ]

  // deleting from feedback node
  getLikeFromUser(id: userId, postId: post.key) { like in
     if like.userId != like.postAddedByUserId {
        updates.updateValue(nil, forKey: "/feedback/" + like.postAddedByUserId + "/" + like.key)
     }

     ref.updateChildValues(updates)
  }
}

static func getLikeFromUser(id: String, postId: String,
                           completion: @escaping (_ likeId: LikeItem) -> Void) {
  let refToLike = ref.child("/userslikes/" + id + "/onposts/" + postId)

  refToLike.observeSingleEvent(of: .value, with: { snapshot in
     let like = LikeItem(snapshot: snapshot)

     completion(like)
  })
}

So, when user taps "remove like" I have some delay (It is fetching like entity to get feedback id at this time).

And the problem: If I'm spamming like-removeLike button (like - remove like - l - rl - l - rl etc.), sometimes my feedback node is duplicating (with different keys ofc. It has not removed old node) and sometimes it is not adding (in this situation it is crashing if I try to remove it in the future).

How to fix it?


Solution

  • My humble opinion, first of all this could be fix with UX limitations. User shouldn't be able to spam any button in application. Must be a delay between this events. Even you can add some max. switch between user decisions... wait a while and make it free again (maybe).

    Like you said on your comment, it's very good idea and good UX wait user until finish write operation. This way you can eliminate bad UX.

    You can use userinteractionenabled property of UIView.

    When set to NO, touch, press, keyboard, and focus events intended for the view are ignored and removed from the event queue. When set to YES, events are delivered to the view normally. The default value of this property is YES.

    During an animation, user interactions are temporarily disabled for all views involved in the animation, regardless of the value in this property. You can disable this behavior by specifying the UIViewAnimationOptionAllowUserInteraction option when configuring the animation.

    Of course there are many alternatives, sky is the limit for UX scenario.

    Also you can check Apple's user interface guidelines for loading:

    https://developer.apple.com/ios/human-interface-guidelines/interaction/loading/

    Show content as soon as possible. Don’t make people wait for content to load before seeing the screen they're expecting. Show the screen immediately, and use placeholder text, graphics, or animations to identify where content isn't available yet. Replace these placeholder elements as the content loads. Whenever possible, preload upcoming content in the background, such as while an animation is playing or the user is navigating a level or menu.

    and indicators maybe:

    https://developer.apple.com/ios/human-interface-guidelines/ui-controls/progress-indicators/

    If it’s helpful, provide useful information while waiting for a task to complete. Include a label above an activity indicator to give extra context. Avoid vague terms like loading or authenticating because they don’t usually add any value.

    Another option

    Like you said in your comment below there is another option to keep like/dislike until user lives the ViewController. But there is another UX problem that when user try to close modal or back to previous view controller they will wait until this background job finish. Another issue if user kills the application you have 1 change left to save data and it's AppDelegate's applicationWillTerminate. But it's bad practice to save data there because 5 seconds limit:

    https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623111-applicationwillterminate

    This method lets your app know that it is about to be terminated and purged from memory entirely. You should use this method to perform any final clean-up tasks for your app, such as freeing shared resources, saving user data, and invalidating timers. Your implementation of this method has approximately five seconds to perform any tasks and return.

    If the method does not return before time expires, the system may kill the process altogether. For apps that do not support background execution or are linked against iOS 3.x or earlier, this method is always called when the user quits the app. For apps that support background execution, this method is generally not called when the user quits the app because the app simply moves to the background in that case. However, this method may be called in situations where the app is running in the background (not suspended) and the system needs to terminate it for some reason. After calling this method, the app also posts a UIApplicationWillTerminate notification to give interested objects a chance to respond to the transition.

    Hope it helps.