Last active
September 17, 2018 09:29
-
-
Save rolandleth/39161cc2ef03202cf69b74f6c413505b to your computer and use it in GitHub Desktop.
Interactive label
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 | |
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 |
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 | |
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