-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
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):
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
}
}
}