Skip to content

Instantly share code, notes, and snippets.

@Kirow
Created June 11, 2019 11:17
Show Gist options
  • Save Kirow/be0db23df134efda3b34fb507232d1ab to your computer and use it in GitHub Desktop.
Save Kirow/be0db23df134efda3b34fb507232d1ab to your computer and use it in GitHub Desktop.
Simple UILabel subclass with clickable text
public class InteractiveLabel: UILabel {
private struct InteractiveText: Equatable {
let range: Range<String.Index>
let defaultColor: UIColor
let highlightedColor: UIColor
let action: () -> Void
static func == (lhs: InteractiveText, rhs: InteractiveText) -> Bool {
return lhs.range == rhs.range
}
}
private var manager: NSLayoutManager!
private var storage: NSTextStorage!
private var container: NSTextContainer!
private var interactiveList: [InteractiveText] = []
private var lastSelection: InteractiveText?
public override var attributedText: NSAttributedString? {
didSet {
updateTouchDetector()
}
}
public func addInteractiveText(_ interactiveText: String, defaultColor: UIColor? = nil,
highlightedColor: UIColor? = nil, action: @escaping () -> Void) {
guard let text = self.text, let range = text.range(of: interactiveText) else {
return
}
addInteractiveText(range: range, defaultColor: defaultColor, highlightedColor: highlightedColor, action: action)
}
public func addInteractiveText(range: Range<String.Index>, defaultColor: UIColor? = nil,
highlightedColor: UIColor? = nil, action: @escaping () -> Void) {
interactiveList.removeAll(where: {$0.range.overlaps(range)})
let interactiveText = InteractiveText(range: range, defaultColor: defaultColor ?? textColor,
highlightedColor: highlightedColor ?? textColor, action: action)
interactiveList.append(interactiveText)
highlightInteractiveText(false, text: interactiveText)
}
private func updateTouchDetector() {
guard let attributedText = attributedText else {
return
}
storage = NSTextStorage(attributedString: attributedText)
manager = NSLayoutManager()
storage.addLayoutManager(manager)
container = NSTextContainer(size: frame.size) //this frame will be invalid during init
container.lineFragmentPadding = 0.0
container.lineBreakMode = lineBreakMode
container.maximumNumberOfLines = numberOfLines
manager.addTextContainer(container)
}
private func highlightInteractiveText(_ highlight: Bool, text: InteractiveText) {
let attributedText = self.attributedText ?? NSAttributedString(string: self.text ?? "")
let modifiedText = NSMutableAttributedString(attributedString: attributedText)
let nsRange = NSRange(text.range, in: attributedText.string)
let color = highlight ? text.highlightedColor : text.defaultColor
modifiedText.addAttribute(.foregroundColor, value: color, range: nsRange)
self.attributedText = modifiedText
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard interactiveList.count > 0, let touchLocation = touches.first?.location(in: self) else {
return
}
let indexOfCharacter = manager.characterIndex(for: touchLocation, in: container, fractionOfDistanceBetweenInsertionPoints: nil)
guard indexOfCharacter != NSNotFound, let text = self.text,
let selection = interactiveList.first(where: {$0.range ~= text.index(text.startIndex, offsetBy: indexOfCharacter) }) else {
return
}
highlightInteractiveText(true, text: selection)
lastSelection = selection
}
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
guard let last = lastSelection, let touchLocation = touches.first?.location(in: self) else {
return
}
let indexOfCharacter = manager.characterIndex(for: touchLocation, in: container, fractionOfDistanceBetweenInsertionPoints: nil)
if indexOfCharacter != NSNotFound, let text = self.text,
last.range.contains(text.index(text.startIndex, offsetBy: indexOfCharacter)) {
highlightInteractiveText(true, text: last)
} else {
highlightInteractiveText(false, text: last)
}
}
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
guard let last = lastSelection, let touchLocation = touches.first?.location(in: self) else {
return
}
let indexOfCharacter = manager.characterIndex(for: touchLocation, in: container, fractionOfDistanceBetweenInsertionPoints: nil)
if indexOfCharacter != NSNotFound, let text = self.text,
last.range.contains(text.index(text.startIndex, offsetBy: indexOfCharacter)) {
last.action()
}
highlightInteractiveText(false, text: last)
lastSelection = nil
}
public override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
guard let last = lastSelection else {
return
}
highlightInteractiveText(false, text: last)
lastSelection = nil
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment