Skip to content

Instantly share code, notes, and snippets.

@jeandavid
Created February 14, 2021 11:22
Show Gist options
  • Save jeandavid/af66569440c8b336fe3ac29caac5e2d1 to your computer and use it in GitHub Desktop.
Save jeandavid/af66569440c8b336fe3ac29caac5e2d1 to your computer and use it in GitHub Desktop.
A UILabel that responds to embedded link tap
import UIKit
/// A UILabel subclass that responds to links tap.
public class LinkableLabel: UILabel {
// MARK: - Private
private var touchedLink: URL?
@objc
private func handleTouch(_ sender: UIGestureRecognizer) {
if let link = touchedLink, let linkHandler = linkHandler {
linkHandler(link, sender)
} else {
tapHandler?(sender)
}
}
private var alignmentOffset: CGFloat {
switch textAlignment {
case .left, .natural, .justified:
return 0.0
case .center:
return 0.5
case .right:
return 1.0
default:
return 0
}
}
private var needsLinkColorUpdate: Bool = true
/// Set the text color for links.
/// - Important: One can't change the color for a `link`. We need to use another attribute type. In our case, we use `attachment`.
private func setLinkColor(color: UIColor) -> NSAttributedString? {
guard let attributedText = attributedText else { return nil }
let mutableCopy = NSMutableAttributedString(attributedString: attributedText)
let range = NSRange(location: 0, length: mutableCopy.length)
mutableCopy.enumerateAttributes(in: range, options: []) { (attributes, range, _) in
var currentAttributes = mutableCopy.attributes(at: range.location, longestEffectiveRange: nil, in: range)
if let link = currentAttributes[NSAttributedString.Key.link] as? String, let url = URL(string: link) {
currentAttributes.removeValue(forKey: .link)
currentAttributes[.attachment] = url
currentAttributes[.foregroundColor] = color
mutableCopy.setAttributes(currentAttributes, range: range)
} else if let _ = currentAttributes[NSAttributedString.Key.attachment] as? URL {
currentAttributes[.foregroundColor] = color
mutableCopy.setAttributes(currentAttributes, range: range)
}
}
return mutableCopy
}
// MARK: - API
/// Set the text color for links in the attributedText property
public var linkColor: UIColor? {
didSet {
guard let linkColor = linkColor else { return }
needsLinkColorUpdate = false
attributedText = setLinkColor(color: linkColor)
needsLinkColorUpdate = true
}
}
/// Use tapHandler for a unique gesture accross all text. Does not differentiate between links.
public var tapHandler: ((UIGestureRecognizer) -> Void)?
/// Use linkHandler for opening links in the attributedText property.
public var linkHandler: ((URL, UIGestureRecognizer) -> Void)?
// MARK: - Init
public override init(frame: CGRect) {
super.init(frame: frame)
let rec = UITapGestureRecognizer(target: self, action: #selector(handleTouch(_:)))
addGestureRecognizer(rec)
isUserInteractionEnabled = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override var attributedText: NSAttributedString? {
didSet {
guard let linkColor = linkColor, needsLinkColorUpdate else { return }
let mutableCopy = setLinkColor(color: linkColor)
if mutableCopy != attributedText {
attributedText = mutableCopy
}
}
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let attributedText = attributedText, super.hitTest(point, with: event) != nil else { return nil }
let size = bounds.size
let textContainer = NSTextContainer(size: size)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = .byWordWrapping
textContainer.maximumNumberOfLines = numberOfLines
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
layoutManager.usesFontLeading = false
let textStorage = NSTextStorage(attributedString: attributedText)
textStorage.addLayoutManager(layoutManager)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let xOffset = ((size.width - textBoundingBox.size.width) * alignmentOffset) - textBoundingBox.origin.x
let yOffset = ((size.height - textBoundingBox.size.height) * alignmentOffset) - textBoundingBox.origin.y
let locationOfTouchInTextContainer = CGPoint(x: point.x - xOffset, y: point.y - yOffset)
let tappedCharacterIndex: Int = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
if tappedCharacterIndex < 0 || tappedCharacterIndex >= attributedText.string.count {
touchedLink = nil
return nil
}
let attributesAtTappedIndex = attributedText.attributes(at: tappedCharacterIndex, effectiveRange: nil)
if let absoluteLink = attributesAtTappedIndex[NSAttributedString.Key.link] as? String, let link = URL(string: absoluteLink) {
touchedLink = link
return self
} else if let link = attributesAtTappedIndex[NSAttributedString.Key.attachment] as? URL {
touchedLink = link
return self
} else if let link = attributesAtTappedIndex[NSAttributedString.Key(rawValue: "href")] as? URL {
touchedLink = link
return self
} else if tapHandler != nil {
touchedLink = nil
return self
} else {
touchedLink = nil
return nil
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment