Skip to content

Triggering didTapMessageTopLabel only on text #1888

@afsmarques

Description

@afsmarques

Quick question: is it possible to trigger didTapMessageTopLabel only when the user taps directly on the text?

In my case, the top label shows a date/time, and when it’s tapped, I want to push a new view controller. I already have similar logic when tapping the message bubble, and in that case, the tap is only detected within the bubble—which is exactly what I want.

However, for the top label, didTapMessageTopLabel is triggered even when tapping areas without text, as long as it's within the label's bounds.

I found a workaround by using cellForItemAt and adding a custom gesture recognizer to each cell, then checking if the tap occurred on the actual text. It works, but I’m not happy with this approach since it feels a bit hacky.

Has anyone else faced this issue? Is there a cleaner way to detect taps only on the text inside the top label?

This is my UI (the red parts are where the default delegate methods detect touches):

Image

and my approach:

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = super.collectionView(collectionView, cellForItemAt: indexPath)

    if let messageCell = cell as? MessageContentCell {
        let label = messageCell.messageTopLabel
        label.isUserInteractionEnabled = true

        // Avoid adding multiple recognizers
        if label.gestureRecognizers?.isEmpty ?? true {
            let gesture = UITapGestureRecognizer(target: self, action: #selector(handleTopLabelTap(_:)))
            label.addGestureRecognizer(gesture)
        }
    }

    return cell
}

@objc func handleTopLabelTap(_ gesture: UITapGestureRecognizer) {
    guard let label = gesture.view as? UILabel else { return }
    guard let attributedText = label.attributedText else { return }

    let layoutManager = NSLayoutManager()
    let textStorage = NSTextStorage(attributedString: attributedText)
    let textContainer = NSTextContainer(size: label.bounds.size)

    layoutManager.addTextContainer(textContainer)
    textStorage.addLayoutManager(layoutManager)

    textContainer.lineFragmentPadding = 0
    textContainer.maximumNumberOfLines = label.numberOfLines
    textContainer.lineBreakMode = label.lineBreakMode

    layoutManager.ensureLayout(for: textContainer)

    // Get the insets
    let insets: UIEdgeInsets
    if let insetLabel = label as? InsetLabel {
       insets = insetLabel.textInsets
    } else {
       insets = .zero
    }

    // Get tap location adjusted for insets
    let rawLocation = gesture.location(in: label)
    var adjustedPoint = CGPoint(
        x: rawLocation.x - insets.left,
        y: rawLocation.y - insets.top
    )

    // Calculate text bounding rect
    let glyphRange = layoutManager.glyphRange(for: textContainer)
    let boundingRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)

    // Adjust for vertical alignment
    adjustedPoint.y -= (label.bounds.height - insets.top - insets.bottom - boundingRect.height) / 2

    // Adjust for horizontal alignment
    switch label.textAlignment {
    case .center:
        adjustedPoint.x -= (label.bounds.width - insets.left - insets.right - boundingRect.width) / 2
    case .right:
        adjustedPoint.x -= (label.bounds.width - insets.left - insets.right - boundingRect.width)
    default:
        break // Left: no adjustment
    }

    // Final check: is the tap within actual text rect?
    if boundingRect.contains(adjustedPoint) {
        var fraction: CGFloat = 0
        let index = layoutManager.characterIndex(for: adjustedPoint, in: textContainer, fractionOfDistanceBetweenInsertionPoints: &fraction)

        if index < attributedText.length {
            // TODO: Call something
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions