Skip to content

Instantly share code, notes, and snippets.

@rolandleth
Last active September 17, 2018 09:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rolandleth/39161cc2ef03202cf69b74f6c413505b to your computer and use it in GitHub Desktop.
Save rolandleth/39161cc2ef03202cf69b74f6c413505b to your computer and use it in GitHub Desktop.
Interactive label
import UIKit
import PlaygroundSupport
final class FullInteractiveLabel: UILabel {
private let dataDetector: NSDataDetector?
private var detectedResults: [NSTextCheckingResult] {
guard let text = attributedText?.string ?? self.text else { return [] }
return dataDetector?.matches(in: text, range: NSRange(location: 0, length: text.count)) ?? []
}
private let tapGesture = UITapGestureRecognizer()
private let layoutManager = NSLayoutManager()
private let textContainer = NSTextContainer(size: .zero)
private let textStorage = NSTextStorage()
override var text: String? {
didSet {
guard !detectedResults.isEmpty else { return }
guard let text = self.text else { return }
attributedText = NSAttributedString(string: text,
attributes: [.foregroundColor: textColor, .font: font])
}
}
override var attributedText: NSAttributedString? {
didSet {
let results = detectedResults
guard !results.isEmpty else { return }
guard let oldText = attributedText, !oldText.string.isEmpty else {
textStorage.setAttributedString(NSAttributedString())
return
}
let mutableText = NSMutableAttributedString(attributedString: oldText)
results.forEach {
mutableText.addAttribute(.foregroundColor, value: resultsColor, range: $0.range)
}
guard oldText != mutableText else {
textStorage.setAttributedString(mutableText)
return
}
attributedText = mutableText
}
}
override var lineBreakMode: NSLineBreakMode {
didSet {
textContainer.lineBreakMode = lineBreakMode
}
}
override var numberOfLines: Int {
didSet {
textContainer.maximumNumberOfLines = numberOfLines
}
}
private let resultsColor: UIColor
private var detectedResultTapped: (NSTextCheckingResult, String) -> Void = { _, _ in }
// MARK: - Callbacks
func onDetectedDataTap(execute work: @escaping (NSTextCheckingResult, String) -> Void) {
detectedResultTapped = work
}
private func didTapDetectedResult(_ result: NSTextCheckingResult, on text: String) {
detectedResultTapped(result, text)
}
// MARK: - Update
override func layoutSubviews() {
super.layoutSubviews()
textContainer.size = bounds.size
}
@objc
private func tapped(_ gesture: UITapGestureRecognizer) {
gesture.cancelsTouchesInView = false
let results = detectedResults
guard !results.isEmpty else { return }
guard gesture.state == .ended else { return }
guard let text = self.text else { return }
let touchLocation = gesture.location(in: gesture.view)
let indexOfCharacter = layoutManager.characterIndex(for: touchLocation,
in: textContainer,
fractionOfDistanceBetweenInsertionPoints: nil)
for result in results {
guard let range = Range(result.range, in: text) else { continue }
guard result.range.contains(indexOfCharacter) else { continue }
gesture.cancelsTouchesInView = true
didTapDetectedResult(result, on: String(text[range]))
return
}
}
// MARK: - Init
init(resultsColor: UIColor = .red, dataTypes: NSTextCheckingTypes) {
dataDetector = try? NSDataDetector(types: dataTypes)
self.resultsColor = resultsColor
super.init(frame: .zero)
backgroundColor = .white
textColor = .darkText
isUserInteractionEnabled = true
numberOfLines = 0
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
tapGesture.addTarget(self, action: #selector(tapped))
addGestureRecognizer(tapGesture)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
let label = FullInteractiveLabel(resultsColor: .blue, dataTypes: NSTextCheckingResult.CheckingType.link.rawValue | NSTextCheckingResult.CheckingType.date.rawValue)
label.backgroundColor = .white
label.font = .systemFont(ofSize: 14)
label.text = "My blog is located at https://rolandleth.com.\nThis is where I write from time to time.\nI wrote this class Friday, 19 January 2018."
label.numberOfLines = 3
label.sizeToFit()
label.onDetectedDataTap { result, text in
print(result.date)
print(result.url)
print(text)
}
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = label
import UIKit
final class AttributedLabel: UILabel {
private let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
private var detectedResults: [NSTextCheckingResult] {
guard let text = attributedText?.string ?? self.text else { return [] }
return dataDetector?.matches(in: text, range: NSRange(location: 0, length: text.count)) ?? []
}
private let tapGesture = UITapGestureRecognizer()
private let layoutManager = NSLayoutManager()
private let textContainer = NSTextContainer(size: .zero)
private let textStorage = NSTextStorage()
override var text: String? {
didSet {
guard !detectedResults.isEmpty else { return }
guard let text = self.text else { return }
attributedText = NSAttributedString(string: text,
attributes: [.foregroundColor: textColor, .font: font])
}
}
override var attributedText: NSAttributedString? {
didSet {
guard !detectedResults.isEmpty else { return }
guard let oldText = self.attributedText, !oldText.string.isEmpty else {
textStorage.setAttributedString(NSAttributedString())
return
}
let mutableText = NSMutableAttributedString(attributedString: oldText)
detectedResults.forEach {
mutableText.addAttribute(.foregroundColor, value: linkColor, range: $0.range)
}
guard oldText != mutableText else {
textStorage.setAttributedString(mutableText)
return
}
attributedText = mutableText
}
}
override var lineBreakMode: NSLineBreakMode {
didSet {
textContainer.lineBreakMode = lineBreakMode
}
}
override var numberOfLines: Int {
didSet {
textContainer.maximumNumberOfLines = numberOfLines
}
}
private let linkColor: UIColor
private var linkTapped: (_ url: URL) -> Void = { _ in }
// MARK: - Callbacks
func onLinkTap(execute work: @escaping (URL) -> Void) {
linkTapped = work
}
private func didTapLink(_ url: URL) {
linkTapped(url)
}
// MARK: - Update
override func layoutSubviews() {
super.layoutSubviews()
textContainer.size = bounds.size
}
@objc
private func tapped(_ gesture: UITapGestureRecognizer) {
gesture.cancelsTouchesInView = false
let results = detectedResults
guard !results.isEmpty else { return }
guard gesture.state == .ended else { return }
let touchLocation = gesture.location(in: gesture.view)
let indexOfCharacter = layoutManager.characterIndex(for: touchLocation,
in: textContainer,
fractionOfDistanceBetweenInsertionPoints: nil)
for result in results {
guard result.range.contains(indexOfCharacter) else { continue }
guard let url = result.url else { continue }
gesture.cancelsTouchesInView = true
didTapLink(url)
return
}
}
// MARK: - Init
init(linkColor: UIColor = .red) {
self.linkColor = linkColor
super.init(frame: .zero)
backgroundColor = .white
textColor = .dark
isUserInteractionEnabled = true
numberOfLines = 0
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
tapGesture.addTarget(self, action: #selector(tapped))
addGestureRecognizer(tapGesture)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment