Created
June 11, 2019 11:17
-
-
Save Kirow/be0db23df134efda3b34fb507232d1ab to your computer and use it in GitHub Desktop.
Simple UILabel subclass with clickable text
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
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