Tusker/Tusker/Views/LinkLabel.swift

182 lines
6.0 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)
]
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 = 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
}
}