Reaction View in iMessage Implementation

I've implemented an emoji "quick reaction" feature for our messaging feature within our app (using Swift). The reaction UI/UX is supposed to match that of the quick emoji reaction via long press gesture in iMessage almost exactly, aside from which reactions we chose to display.

We've implemented this feature using the func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration ...). Within this delegate function we create a container view that has a snapshot of the message TableViewCell with the reaction UIView inserted right above the snapshot, like it does visually in iMessage.

This container is passed in to the UITargetedPreview(view: /*here*/, parameters: parameters, target: previewTarget) constructor and is then returned for the aforementioned delegate function.

This works functionally, but here are some issues with this UI/UX wise that is different than iMessage, which I will describe:

  1. When the user "long presses" to trigger the ContextMenu, it reacts to the press very quickly, i.e duration of fingertip "pressing" is short as ~0.25 seconds, where as a traditional UIGestureRecognizer takes longer than ~1.0 seconds. We'd like to make the required duration of physical "pressing" to be more around 0.5 - 1.0 seconds like in iMessage.

  2. When the user initiates the ContextMenu within iMessage, the reactions view and the ContextMenu appear at the same exact time. In our implementation, presuming because the cell snapshot and reaction view are within the same container, they both appear prior to the context menu. The reason why this happens makes sense to me, however I would like to know how iMessage implements the Context Menu functionality in iMessage, such that the reaction view appears with the Context Menu. The user seeing only the message first, then both menu elements.

Note: We use the UITargetedPreview so that we can specify a .clear background color for the targetView, as the same UI/UX as in iMessage. We did not find another way to implement that design.

In conclusion, in what way does iMessage present Context Menu's such that the the following feature requirements are fullfilled: Background is clear between the reaction view, the message, and the context menu. The duration of gesture press that is required to trigger the menu being more than ~0.5 seconds. As well as the message cell appearing first, and then subsequently the reaction view + context menu together?

  • Addendum: How would I accomplish what I described above?

  • Hi there! I have similar task and I wasn't able to find sample code where UITargetedPreview is initialised with previewTarget. My question, how to make view (targetedPreview.view) transparent. I tried to do it in this way: let previewParams = UIPreviewParameters(); previewParams.backgroundColor = .clear; previewParams.visiblePath = UIBezierPath(roundedRect: myContainerView.frame, cornerRadius: 20). But it doesn't work.Could you please show me your implementation or sample?

  • @Kirill-Avdeenko I posted below how I do this.

Add a Comment

Accepted Reply

I was able to solve the second problem listed (#2.) by used the UITableViewDelegate method func tableView(_ tableView: UITableView, willDisplayContextMenu configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?). I did this by setting the reaction's view isHidden property to true during setup and then subsequently adding an animation to the animator of the delegate function willDisplayContextMenuto set isHidden back to false. This makes it so the reaction view appears at the same time as the ContextMenu!

As for the duration of the long press, I have decided that was ancillary and of low priority which. with this above solution, is not as much of importance. Hope this helps some devs out there.

Replies

I was able to solve the second problem listed (#2.) by used the UITableViewDelegate method func tableView(_ tableView: UITableView, willDisplayContextMenu configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?). I did this by setting the reaction's view isHidden property to true during setup and then subsequently adding an animation to the animator of the delegate function willDisplayContextMenuto set isHidden back to false. This makes it so the reaction view appears at the same time as the ContextMenu!

As for the duration of the long press, I have decided that was ancillary and of low priority which. with this above solution, is not as much of importance. Hope this helps some devs out there.

Can you share your code snippet for this reaction view? Couldn't figure out how to place a view above the table cell with the reaction cloud

  • Sure, I replied with code snippet on how to do this below.

Add a Comment

As per request of commenter's of this post, this is how I set up the container view which holds the reactionView for the cell:

func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
        guard
            let indexPath = configuration.identifier as? IndexPath,
            let cell = tableView.cellForRow(at: indexPath) as? MessageTableViewCell
        else {
            return nil
        }
        
        ctxMenuReactionsView = ReactionsView.setup()
        ctxMenuReactionsView?.isHidden = true /* must set isHidden = false in the delegate method I mentioned in checkmark'd answer */
        
        if let snapshot = cell.snapshotView(afterScreenUpdates: false) { /* snapshot the view of the cell for displaying in container */
            snapshot.isHidden = false
            snapshot.layer.cornerRadius = 10
            snapshot.layer.masksToBounds = true
            snapshot.translatesAutoresizingMaskIntoConstraints = false

            /* create the container that the snapshot and reactionView will be in */
            let container = UIView(frame: CGRect(
                origin: .zero,
                size: CGSize(
                    width: cell.bounds.width,
                    height: cell.bounds.height + ctxMenuReactionsView!.bounds.height + 5
                )
            ))
            container.backgroundColor = .clear
            container.addSubview(ctxMenuReactionsView!)
            container.addSubview(snapshot)
            
            /* set up constraints for snapshot and ReactionsView */
            ctxMenuReactionsView?.leftAnchor.constraint(equalTo: container.leftAnchor).isActive = true
            ctxMenuReactionsView?.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
            ctxMenuReactionsView?.widthAnchor.constraint(...)
            
            container.addConstraints([
            ...
            ])
            
            var centerPoint = CGPoint(x: cell.center.x, y: cell.center.y - ctxMenuReactionsView!.bounds.height)
            let windowHeight =  self.view.window?.size.height ?? 0

            if snapshot.bounds.height > (windowHeight * 0.9) { /* if the snapshot of cell if too tall, we use this center point to make it fit */
                centerPoint = CGPoint(x: cell.center.x, y: tableView.center.y)
            }
            
            let previewTarget = UIPreviewTarget(container: tableView, center: centerPoint)

            /* makes sure background of the container is clear */
            let parameters = UIPreviewParameters()
            parameters.backgroundColor = .clear

            if #available(iOS 14.0, *) { /* this removes the shadow from the container */
                parameters.shadowPath = UIBezierPath()
            }
            
            return UITargetedPreview(view: container, parameters: parameters, target: previewTarget)
}

Be sure to pass in the indexPath of the cell in the func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) - UIContextMenuConfiguration? function in the identifier paramater like so:

return UIContextMenuConfiguration(identifier: indexPath as NSCopying,
                                          previewProvider: nil,
                                          actionProvider: 
{
 ... set up menu items code
}