Created
February 14, 2021 11:22
-
-
Save jeandavid/af66569440c8b336fe3ac29caac5e2d1 to your computer and use it in GitHub Desktop.
A UILabel that responds to embedded link tap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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