Created
April 6, 2023 07:59
-
-
Save 13hoop/179adf74d8129dc061755de04480260e to your computer and use it in GitHub Desktop.
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
// | |
// HyperlinkLabel.swift | |
// SLHome | |
// | |
// Created by yongren on 2022/1/6. | |
// | |
import Foundation | |
/// A UILabel subclass that responds to links tap. | |
public class YRHyperlinkLabel: UILabel { | |
// MARK: - Private | |
private var touchedLink: URL? | |
@objc | |
private func handleTouch(_ sender: UIGestureRecognizer) { | |
if let link = touchedLink, let linkHandler = linkHandler { | |
linkHandler(link, sender) | |
} else { | |
tapHandler?(sender) | |
} | |
} | |
private var alignmentOffset: CGFloat { | |
switch textAlignment { | |
case .left, .natural, .justified: | |
return 0.0 | |
case .center: | |
return 0.5 | |
case .right: | |
return 1.0 | |
default: | |
return 0 | |
} | |
} | |
private var needsLinkColorUpdate: Bool = true | |
/// Set the text color for links. | |
/// - Important: One can't change the color for a `link`. We need to use another attribute type. In our case, we use `attachment`. | |
private func setLinkColor(color: UIColor) -> NSAttributedString? { | |
guard let attributedText = attributedText else { return nil } | |
let mutableCopy = NSMutableAttributedString(attributedString: attributedText) | |
let range = NSRange(location: 0, length: mutableCopy.length) | |
mutableCopy.enumerateAttributes(in: range, options: []) { (attributes, range, _) in | |
var currentAttributes = mutableCopy.attributes(at: range.location, longestEffectiveRange: nil, in: range) | |
if let link = currentAttributes[NSAttributedString.Key.link] as? String, let url = URL(string: link) { | |
currentAttributes.removeValue(forKey: .link) | |
currentAttributes[.attachment] = url | |
currentAttributes[.foregroundColor] = color | |
mutableCopy.setAttributes(currentAttributes, range: range) | |
} else if currentAttributes[NSAttributedString.Key.attachment] != nil { | |
currentAttributes[.foregroundColor] = color | |
mutableCopy.setAttributes(currentAttributes, range: range) | |
} | |
} | |
return mutableCopy | |
} | |
// MARK: - API | |
/// Set the text color for links in the attributedText property | |
public var linkColor: UIColor? { | |
didSet { | |
guard let linkColor = linkColor else { return } | |
needsLinkColorUpdate = false | |
attributedText = setLinkColor(color: linkColor) | |
needsLinkColorUpdate = true | |
} | |
} | |
/// Use tapHandler for a unique gesture accross all text. Does not differentiate between links. | |
public var tapHandler: ((UIGestureRecognizer) -> Void)? | |
/// Use linkHandler for opening links in the attributedText property. | |
public var linkHandler: ((URL, UIGestureRecognizer) -> Void)? | |
// MARK: - Init | |
public override init(frame: CGRect) { | |
super.init(frame: frame) | |
let rec = UITapGestureRecognizer(target: self, action: #selector(handleTouch(_:))) | |
addGestureRecognizer(rec) | |
isUserInteractionEnabled = true | |
} | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
public override var attributedText: NSAttributedString? { | |
didSet { | |
guard let linkColor = linkColor, needsLinkColorUpdate else { return } | |
let mutableCopy = setLinkColor(color: linkColor) | |
if mutableCopy != attributedText { | |
attributedText = mutableCopy | |
} | |
} | |
} | |
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { | |
guard let attributedText = attributedText, super.hitTest(point, with: event) != nil else { return nil } | |
let size = bounds.size | |
let textContainer = NSTextContainer(size: size) | |
textContainer.lineFragmentPadding = 0.0 | |
textContainer.lineBreakMode = .byWordWrapping | |
textContainer.maximumNumberOfLines = numberOfLines | |
let layoutManager = NSLayoutManager() | |
layoutManager.addTextContainer(textContainer) | |
layoutManager.usesFontLeading = false | |
let textStorage = NSTextStorage(attributedString: attributedText) | |
textStorage.addLayoutManager(layoutManager) | |
let textBoundingBox = layoutManager.usedRect(for: textContainer) | |
let xOffset = ((size.width - textBoundingBox.size.width) * alignmentOffset) - textBoundingBox.origin.x | |
let yOffset = ((size.height - textBoundingBox.size.height) * alignmentOffset) - textBoundingBox.origin.y | |
let locationOfTouchInTextContainer = CGPoint(x: point.x - xOffset, y: point.y - yOffset) | |
let tappedCharacterIndex: Int = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) | |
if tappedCharacterIndex < 0 || tappedCharacterIndex >= attributedText.string.count { | |
touchedLink = nil | |
return nil | |
} | |
let attributesAtTappedIndex = attributedText.attributes(at: tappedCharacterIndex, effectiveRange: nil) | |
if let link = attributesAtTappedIndex[NSAttributedString.Key.link] as? URL { | |
touchedLink = link | |
return self | |
} else if let absoluteLink = attributesAtTappedIndex[NSAttributedString.Key.link] as? String, let link = URL(string: absoluteLink) { | |
touchedLink = link | |
return self | |
} else if let link = attributesAtTappedIndex[NSAttributedString.Key.attachment] as? URL { | |
touchedLink = link | |
return self | |
} else if let link = attributesAtTappedIndex[NSAttributedString.Key(rawValue: "href")] as? URL { | |
touchedLink = link | |
return self | |
} else if tapHandler != nil { | |
touchedLink = nil | |
return self | |
} else { | |
touchedLink = nil | |
return nil | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hyperlink Label by [email protected]