diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 4f56c272..eafd440a 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -402,6 +402,36 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } } + /** + Provides a text container and layout manager of how the text would appear on screen. + They are used in tandem to derive low-level TextKit results of the label. + */ + public func abstractTextContainer() -> (textContainer: NSTextContainer, layoutManager: NSLayoutManager, textStorage: NSTextStorage)? { + + // Must configure the attributed string to translate what would appear on screen to accurately analyze. + guard let attributedText = attributedText else { return nil } + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = textAlignment + + let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText) + stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count)) + + let textStorage = NSTextStorage(attributedString: stagedAttributedString) + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: .zero) + + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + textContainer.lineFragmentPadding = 0.0 + textContainer.lineBreakMode = lineBreakMode + textContainer.maximumNumberOfLines = numberOfLines + textContainer.size = bounds.size + + return (textContainer, layoutManager, textStorage) + } + private func customAccessibilityAction(text: String?, range: NSRange, accessibleText: String? = nil) -> UIAccessibilityCustomAction? { guard let text = text, let attributedText else { return nil } diff --git a/VDS/Extensions/UITapGestureRecognizer.swift b/VDS/Extensions/UITapGestureRecognizer.swift index 612a11ca..93fd0d8f 100644 --- a/VDS/Extensions/UITapGestureRecognizer.swift +++ b/VDS/Extensions/UITapGestureRecognizer.swift @@ -15,20 +15,39 @@ extension UITapGestureRecognizer { /// - label: UILabel in question /// - targetRange: Range to look within /// - Returns: Wether the range in the label has an action - public func didTapActionInLabel(_ label: UILabel, inRange targetRange: NSRange) -> Bool { - - guard let attributedText = label.attributedText else { return false } - - let layoutManager = NSLayoutManager() - let textContainer = NSTextContainer(size: label.bounds.size) - let textStorage = NSTextStorage(attributedString: attributedText) - layoutManager.addTextContainer(textContainer) - textStorage.addLayoutManager(layoutManager) - + public func didTapActionInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool { + guard let abstractContainer = label.abstractTextContainer() else { return false } let location = location(in: label) - let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) - - guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, characterIndex < attributedText.length else { return false } - return true + + let textContainer = abstractContainer.textContainer + let layoutManager = abstractContainer.layoutManager + + let indexOfGlyph = layoutManager.glyphIndex(for: location, in: textContainer) + let intrinsicWidth = label.intrinsicContentSize.width + + // Assert that tapped occured within acceptable bounds based on alignment. + switch label.textAlignment { + case .right: + if location.x < label.bounds.width - intrinsicWidth { + return false + } + case .center: + let halfBounds = label.bounds.width / 2 + let halfIntrinsicWidth = intrinsicWidth / 2 + + if location.x > halfBounds + halfIntrinsicWidth { + return false + } else if location.x < halfBounds - halfIntrinsicWidth { + return false + } + default: // Left align + if location.x > intrinsicWidth { + return false + } + } + + // Affirms that the tap occured in the desired rect of provided by the target range. + return layoutManager.boundingRect(forGlyphRange: targetRange, in: textContainer).contains(location) + && NSLocationInRange(indexOfGlyph, targetRange) } }