// // 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, with event: UIEvent?) { if let touch = touches.first, onTouch(touch) { return } super.touchesBegan(touches, with: event) } override func touchesMoved(_ touches: Set, with event: UIEvent?) { if let touch = touches.first, onTouch(touch) { return } super.touchesMoved(touches, with: event) } override func touchesEnded(_ touches: Set, with event: UIEvent?) { if let touch = touches.first, onTouch(touch) { return } super.touchesEnded(touches, with: event) } override func touchesCancelled(_ touches: Set, 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 } }