Skip to content

Instantly share code, notes, and snippets.

@kelvinfok
Created April 28, 2024 13:30
Show Gist options
  • Save kelvinfok/76ac61fe950fa0f736e135a418b1e6d4 to your computer and use it in GitHub Desktop.
Save kelvinfok/76ac61fe950fa0f736e135a418b1e6d4 to your computer and use it in GitHub Desktop.
AttributedTappableLabel
class AttributedTappableLabel: UILabel {
var onTap: (() -> Void)?
var tapRange: NSRange?
var labelFont: UIFont?
override init(frame: CGRect) {
super.init(frame: frame)
// Enable user interaction to handle tap
isUserInteractionEnabled = true
numberOfLines = 0
// Add tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(labelTapped))
addGestureRecognizer(tapGesture)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func labelTapped(sender: UITapGestureRecognizer) {
guard let tapRange = tapRange else { return }
let labelSize = bounds.size
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize(width: labelSize.width, height: CGFloat.greatestFiniteMagnitude))
let textStorage = NSTextStorage(attributedString: attributedText!)
// Add textContainer to layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Find tapped character index
let locationOfTouchInLabel = sender.location(in: self)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x,
y: locationOfTouchInLabel.y - textContainerOffset.y)
let characterIndex = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
// Check if tapped character index is within the tapRange
if NSLocationInRange(characterIndex, tapRange) {
onTap?()
}
}
func setAttributedText(text: String, highlightedText: String?, color: UIColor = .black, font: UIFont = .systemFont(ofSize: 18, weight: .bold)) {
let attributedString = NSMutableAttributedString(string: text)
// Check if highlighted string is provided
if let highlighted = highlightedText {
// Find the range of the highlighted part
let range = (text as NSString).range(of: highlighted)
// Apply the color to the range
attributedString.addAttribute(.foregroundColor, value: color, range: range)
}
attributedString.addAttribute(.font, value: font, range: NSRange(location: 0, length: text.count))
self.attributedText = attributedString
self.tapRange = (highlightedText as NSString?)?.range(of: highlightedText ?? "")
self.labelFont = font
}
init(text: String, font: UIFont) {
super.init(frame: .zero)
numberOfLines = 0
setAttributedText(text: text, highlightedText: "", color: .clear, font: font)
}
func heightForWidth(_ width: CGFloat) -> CGFloat {
guard let font = labelFont, let text = text else { return 0 }
let size = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingRect = NSString(string: text).boundingRect(
with: size,
options: .usesLineFragmentOrigin,
attributes: [NSAttributedString.Key.font: font],
context: nil
)
let safePadding: CGFloat = 4
return boundingRect.height + safePadding
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment