From fea6a2fbbe3d8c20737cac2f507594382c9da11f Mon Sep 17 00:00:00 2001 From: krysto9872 <121132154+krysto9872@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:35:41 -0600 Subject: [PATCH 1/2] Fix UILabel TextKit truncation hit testing Keep TextKit's final-line truncation aligned with UILabel when numberOfLines is limited, and prevent taps on hidden truncated glyphs from triggering actions. Preserve paragraph wrapping for truncating modes and synchronize label layout metrics. --- .../UIKit/UILabel/UILabelExtension.swift | 53 +++++++++++++++++-- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/Sources/Extension/UIKit/UILabel/UILabelExtension.swift b/Sources/Extension/UIKit/UILabel/UILabelExtension.swift index 391cdcf..9cb2890 100644 --- a/Sources/Extension/UIKit/UILabel/UILabelExtension.swift +++ b/Sources/Extension/UIKit/UILabel/UILabelExtension.swift @@ -266,8 +266,14 @@ fileprivate extension UILabel { } func matching(_ point: CGPoint) -> (NSRange, Action)? { - let text = adaptation(scaledAttributedText ?? synthesizedAttributedText ?? attributedText, with: numberOfLines) + let text = adaptation( + scaledAttributedText ?? synthesizedAttributedText ?? attributedText, + with: numberOfLines, + lineBreakMode: lineBreakMode, + textAlignment: textAlignment + ) guard let attributedString = AttributedString(text) else { return nil } + guard attributedString.value.length > 0 else { return nil } // 构建同步Label的TextKit let delegate = UILabelLayoutManagerDelegate(scaledMetrics, with: baselineAdjustment) @@ -302,6 +308,16 @@ fileprivate extension UILabel { // 获取字形下标 var fraction: CGFloat = 0 let glyphIndex = layoutManager.glyphIndex(for: point, in: textContainer, fractionOfDistanceThroughGlyph: &fraction) + let glyphRange = layoutManager.glyphRange(for: textContainer) + guard glyphRange.contains(glyphIndex) else { + return nil + } + // A truncated line can still map a touch to a character that is not + // visible. Do not trigger an action for the hidden part of the text. + let truncatedGlyphRange = layoutManager.truncatedGlyphRange(inLineFragmentForGlyphAt: glyphIndex) + guard truncatedGlyphRange.location == NSNotFound || !truncatedGlyphRange.contains(glyphIndex) else { + return nil + } // 获取字符下标 let index = layoutManager.characterIndexForGlyph(at: glyphIndex) // 通过字形距离判断是否在字形范围内 @@ -431,7 +447,12 @@ extension UILabel { return scaledMetrics?.scaledAttributedText } - private func adaptation(_ string: NSAttributedString?, with numberOfLines: Int) -> NSAttributedString? { + private func adaptation( + _ string: NSAttributedString?, + with numberOfLines: Int, + lineBreakMode: NSLineBreakMode, + textAlignment: NSTextAlignment + ) -> NSAttributedString? { /** 由于富文本中的lineBreakMode对于UILabel和TextKit的行为是不一致的, UILabel默认的.byTruncatingTail在TextKit中则无法正确显示. 所以将富文本中的lineBreakMode全部替换为TextKit默认的.byWordWrapping, 以解决多行显示和不一致的问题. @@ -442,14 +463,36 @@ extension UILabel { } let mutable = NSMutableAttributedString(attributedString: string) + let wrappingMode: NSLineBreakMode + if numberOfLines == 1 { + wrappingMode = .byCharWrapping + } else { + switch lineBreakMode { + case .byTruncatingHead, .byTruncatingMiddle, .byTruncatingTail: + // TextKit performs the final truncation in the text + // container. The paragraph itself must remain wrappable. + wrappingMode = .byWordWrapping + default: + wrappingMode = lineBreakMode + } + } mutable.enumerateAttribute( .paragraphStyle, in: .init(location: 0, length: mutable.length), options: .longestEffectiveRangeNotRequired ) { (value, range, stop) in - guard let old = value as? NSParagraphStyle else { return } - guard let new = old.mutableCopy() as? NSMutableParagraphStyle else { return } - new.lineBreakMode = numberOfLines == 1 ? .byCharWrapping : .byWordWrapping + let new: NSMutableParagraphStyle + if let old = value as? NSParagraphStyle, + let copy = old.mutableCopy() as? NSMutableParagraphStyle { + new = copy + } else { + new = NSMutableParagraphStyle() + new.alignment = textAlignment + } + if new.alignment == .natural { + new.alignment = textAlignment + } + new.lineBreakMode = wrappingMode if #available(iOS 11.0, *) { new.setValue(1, forKey: "lineBreakStrategy") } From 4aa12f635ea9642fad6f20b0d1bc50647f90384c Mon Sep 17 00:00:00 2001 From: krysto9872 <121132154+krysto9872@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:36:35 -0600 Subject: [PATCH 2/2] Align UILabel line metrics with TextKit Keep scaled UILabel baseline, line-height, and paragraph spacing calculations consistent with the TextKit layout used for hit testing. --- .../UIKit/UILabel/UILabelLayoutManagerDelegate.swift | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Sources/Extension/UIKit/UILabel/UILabelLayoutManagerDelegate.swift b/Sources/Extension/UIKit/UILabel/UILabelLayoutManagerDelegate.swift index 72ae4df..057d4cc 100644 --- a/Sources/Extension/UIKit/UILabel/UILabelLayoutManagerDelegate.swift +++ b/Sources/Extension/UIKit/UILabel/UILabelLayoutManagerDelegate.swift @@ -78,7 +78,6 @@ class UILabelLayoutManagerDelegate: NSObject, NSLayoutManagerDelegate { used.size.height = scaledMetrics.scaledSize.height case .alignCenters: - print(scaledMetrics) // 居中的基线偏移 使用Scaled的尺寸高度 var baseline = baselineOffset.pointee // 整行的占用高度 - 缩放的行高 = 上下边距; 上边距 = 上下边距 * 0.5; 居中的基线偏移 = 上边距 + 缩放的基线偏移 @@ -111,13 +110,10 @@ class UILabelLayoutManagerDelegate: NSObject, NSLayoutManagerDelegate { lineFragmentRect.pointee = rect lineFragmentUsedRect.pointee = used - /** - From apple's doc: - true if you modified the layout information and want your modifications to be used or false if the original layout information should be used. - But actually returning false is also used. : ) - We should do this to solve the problem of exclusionPaths not working. - */ - return false + // The rectangles above are the synchronization layer between UILabel + // and TextKit. Returning false tells NSLayoutManager to discard those + // changes and use its proposed geometry instead. + return true } // Implementing this method with a return value 0 will solve the problem of last line disappearing