forked from shadowfacts/Tusker
186 lines
6.1 KiB
Swift
186 lines
6.1 KiB
Swift
//
|
|
// LinkLabel.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 2/3/19.
|
|
// Copyright © 2019 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
class LinkLabel: UILabel {
|
|
|
|
typealias Link = (range: NSRange, url: URL)
|
|
|
|
let layoutManager = NSLayoutManager()
|
|
let textContainer = NSTextContainer(size: .zero)
|
|
var textStorage: NSTextStorage!
|
|
|
|
var links = [Link]()
|
|
|
|
var selectedLinkAttributes: [NSAttributedString.Key: Any] = [
|
|
// .backgroundColor: UIColor(hue: 0, saturation: 0, brightness: 0.9, alpha: 1)
|
|
.backgroundColor: UIColor.secondarySystemBackground
|
|
]
|
|
|
|
var selectedLinkRange: NSRange? {
|
|
didSet {
|
|
if let oldValue = oldValue {
|
|
removeSelectedLinkAttributes(oldValue)
|
|
}
|
|
if let newValue = selectedLinkRange {
|
|
addSelectedLinkAttributes(newValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
override var attributedText: NSAttributedString? {
|
|
didSet {
|
|
guard let attributedText = attributedText else { return }
|
|
|
|
textStorage?.removeLayoutManager(layoutManager)
|
|
|
|
textStorage = NSTextStorage(attributedString: attributedText)
|
|
textStorage.addLayoutManager(layoutManager)
|
|
}
|
|
}
|
|
|
|
override var text: String? {
|
|
willSet {
|
|
fatalError("LinkLabel does not support non-attributed text")
|
|
}
|
|
}
|
|
|
|
override func awakeFromNib() {
|
|
super.awakeFromNib()
|
|
|
|
isUserInteractionEnabled = true
|
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(labelTapped(_:)))
|
|
tapRecognizer.delegate = self
|
|
addGestureRecognizer(tapRecognizer)
|
|
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(labelLongPressed(_:)))
|
|
longPressRecognizer.delegate = self
|
|
addGestureRecognizer(longPressRecognizer)
|
|
|
|
layoutManager.addTextContainer(textContainer)
|
|
textContainer.lineFragmentPadding = 0.0
|
|
textContainer.lineBreakMode = lineBreakMode
|
|
textContainer.maximumNumberOfLines = numberOfLines
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
textContainer.size = bounds.size
|
|
}
|
|
|
|
func getLink(atPoint point: CGPoint) -> Link? {
|
|
let labelSize = bounds.size
|
|
let textBoundingBox = layoutManager.usedRect(for: textContainer)
|
|
let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
|
|
y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
|
|
let locationOfTouchInTextContainer = CGPoint(x: point.x - textContainerOffset.x,
|
|
y: point.y - textContainerOffset.y)
|
|
// let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
|
|
let indexOfCharacter = layoutManager.glyphIndex(for: locationOfTouchInTextContainer, in: textContainer)
|
|
|
|
if let link = links.first(where: { $0.range.contains(indexOfCharacter) }) {
|
|
return link
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func addSelectedLinkAttributes(_ range: NSRange) {
|
|
let mutAttrString = NSMutableAttributedString(attributedString: attributedText!)
|
|
mutAttrString.addAttributes(selectedLinkAttributes, range: range)
|
|
self.attributedText = mutAttrString
|
|
setNeedsDisplay()
|
|
}
|
|
|
|
func removeSelectedLinkAttributes(_ range: NSRange) {
|
|
let mutAttrString = NSMutableAttributedString(attributedString: attributedText!)
|
|
selectedLinkAttributes.keys.forEach { mutAttrString.removeAttribute($0, range: range) }
|
|
self.attributedText = mutAttrString
|
|
setNeedsDisplay()
|
|
}
|
|
|
|
// MARK: - Interaction
|
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
if let touch = touches.first, onTouch(touch) {
|
|
return
|
|
}
|
|
super.touchesBegan(touches, with: event)
|
|
}
|
|
|
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
if let touch = touches.first, onTouch(touch) {
|
|
return
|
|
}
|
|
super.touchesMoved(touches, with: event)
|
|
}
|
|
|
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
if let touch = touches.first, onTouch(touch) {
|
|
return
|
|
}
|
|
super.touchesEnded(touches, with: event)
|
|
}
|
|
|
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
if let touch = touches.first, onTouch(touch) {
|
|
return
|
|
}
|
|
super.touchesCancelled(touches, with: event)
|
|
}
|
|
|
|
func onTouch(_ touch: UITouch) -> Bool {
|
|
let location = touch.location(in: self)
|
|
let link = getLink(atPoint: location)
|
|
|
|
switch touch.phase {
|
|
case .began, .moved:
|
|
selectedLinkRange = link?.range
|
|
case .cancelled, .ended:
|
|
selectedLinkRange = nil
|
|
default:
|
|
break
|
|
}
|
|
|
|
return link != nil
|
|
}
|
|
|
|
@objc func labelTapped(_ recognizer: UITapGestureRecognizer) {
|
|
let location = recognizer.location(in: self)
|
|
guard let link = getLink(atPoint: location) else {
|
|
return
|
|
}
|
|
|
|
linkTapped(link)
|
|
}
|
|
|
|
@objc func labelLongPressed(_ recognizer: UILongPressGestureRecognizer) {
|
|
let location = recognizer.location(in: self)
|
|
guard let link = getLink(atPoint: location) else {
|
|
return
|
|
}
|
|
|
|
linkLongPressed(link)
|
|
}
|
|
|
|
func linkTapped(_ link: Link) {
|
|
}
|
|
|
|
func linkLongPressed(_ link: Link) {
|
|
}
|
|
|
|
}
|
|
|
|
extension LinkLabel: UIGestureRecognizerDelegate {
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
let location = touch.location(in: self)
|
|
let link = getLink(atPoint: location)
|
|
return link != nil
|
|
}
|
|
}
|