如何在UILabel中findCGRect文本的子string?

对于一个给定的NSRange ,我想在UILabel中find一个对应于该NSRange的字形的NSRange 。 例如,我想在“快速的棕色狐狸跳过懒狗”一句中find包含“狗”一词的CGRect

视觉问题描述

诀窍是, UILabel有多行,文本是真正的attributedText文本,所以要findstring的确切位置有点困难。

我想写在我的UILabel子类上的方法看起来像这样:

  - (CGRect)rectForSubstringWithRange:(NSRange)range; 

详情,对于那些有兴趣的人:

我的目标是能够创build一个具有UILabel的确切外观和位置的新UILabel,然后我可以进行animation制作。 其余的我已经知道了,但是特别是这一步让我暂时陷入困境。

到目前为止,我所做的努力来解决这个问题:

  • 我希望在iOS 7中,会有一些文本工具包可以解决这个问题,但是大多数我见过的使用Text Kit的例子都集中在了UITextViewUITextField ,而不是UILabel
  • 我看到另一个关于Stack Overflow的问题,可以解决这个问题,但是接受的答案已经超过两年了,而代码对于归因文本performance不佳。

我敢打赌,正确的答案涉及以下其中之一:

  • 使用标准的文本工具包方法在一行代码中解决这个问题。 我敢打赌,它会涉及NSLayoutManagertextContainerForGlyphAtIndex:effectiveRange
  • 编写一个复杂的方法,将UILabel分解成行,并且可能使用Core Text方法在行内find字形的矩形。 我现在最好的select是分开@ mattt的优秀TTTAttributedLabel ,它有一个方法可以在一个点find一个字形 – 如果我反转,find一个字形的点,可能工作。

更新:这里有一个Github的要点,我已经尝试到目前为止,以解决这个问题的三件事情: https : //gist.github.com/bryanjclark/7036101

按照约书亚在密码中的回答 ,我提出了以下似乎运作良好的问题:

 - (CGRect)boundingRectForCharacterRange:(NSRange)range { NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:[self attributedText]]; NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; [textStorage addLayoutManager:layoutManager]; NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:[self bounds].size]; textContainer.lineFragmentPadding = 0; [layoutManager addTextContainer:textContainer]; NSRange glyphRange; // Convert the range for glyphs. [layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange]; return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer]; } 

以卢克·罗杰斯的答案为基础,但写得很快:

 extension UILabel { func boundingRectForCharacterRange(range: NSRange) -> CGRect? { guard let attributedText = attributedText else { return nil } let textStorage = NSTextStorage(attributedString: attributedText) let layoutManager = NSLayoutManager() textStorage.addLayoutManager(layoutManager) let textContainer = NSTextContainer(size: bounds.size) textContainer.lineFragmentPadding = 0.0 layoutManager.addTextContainer(textContainer) var glyphRange = NSRange() // Convert the range for glyphs. layoutManager.characterRangeForGlyphRange(range, actualGlyphRange: &glyphRange) return layoutManager.boundingRectForGlyphRange(glyphRange, inTextContainer: textContainer) } } 

Swift 3

 extension UILabel { func boundingRectForCharacterRange(range: NSRange) -> CGRect? { guard let attributedText = attributedText else { return nil } let textStorage = NSTextStorage(attributedString: attributedText) let layoutManager = NSLayoutManager() textStorage.addLayoutManager(layoutManager) let textContainer = NSTextContainer(size: bounds.size) textContainer.lineFragmentPadding = 0.0 layoutManager.addTextContainer(textContainer) var glyphRange = NSRange() // Convert the range for glyphs. layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange) return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) } } 

我的build议是使用文本工具包。 不幸的是,我们无法访问UILabel使用的布局pipe理器,但是可能创build它的副本并使用它来获取范围的矩形。

我的build议是创build一个NSTextStorage对象,其中包含与标签中相同的属性文本。 接下来创build一个NSLayoutManager并将其添加到文本存储对象。 最后,创build一个与标签大小相同的NSTextContainer ,并将其添加到布局pipe理器中。

现在文本存储与标签有相同的文本,文本容器的大小与标签的大小相同,所以我们应该能够使用boundingRectForGlyphRange:inTextContainer:来请求为我们的范围创build的布局pipe理器。 确保在布局pipe理器对象上使用glyphRangeForCharacterRange:actualCharacterRange:首先将字符范围转换为字形范围。

一切顺利,应该给你一个你在标签内指定范围的CGRect

我还没有testing过这个,但是这将是我的方法,并且模仿UILabel本身的工作应该有一个很好的成功的机会。

你可以而不是基于你的类在UITextView? 如果是这样,请查看UiTextInput协议方法。 特别参见几何形状和命中rest方法。

不幸的是,在UILabel中没有直接的API。 为了准确地支持这个function,你需要做大量的工作来保持不同angular色的大小。 但是,有人已经花时间来搞清楚了。 请看下面的一段代码,看看是否适合你:

UILabel获取CGRect作为文本的子string

翻译为Swift 3。

 func boundingRectForCharacterRange(_ range: NSRange) -> CGRect { let textStorage = NSTextStorage(attributedString: self.attributedText!) let layoutManager = NSLayoutManager() textStorage.addLayoutManager(layoutManager) let textContainer = NSTextContainer(size: self.bounds.size) textContainer.lineFragmentPadding = 0 layoutManager.addTextContainer(textContainer) var glyphRange = NSRange() layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange) return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) } 

另一个这样做的方法,如果你有自动调整字体大小,将是这样的:

 let stringLength: Int = countElements(self.attributedText!.string) let substring = (self.attributedText!.string as NSString).substringWithRange(substringRange) //First, confirm that the range is within the size of the attributed label if (substringRange.location + substringRange.length > stringLength) { return CGRectZero } //Second, get the rect of the label as a whole. let textRect: CGRect = self.textRectForBounds(self.bounds, limitedToNumberOfLines: self.numberOfLines) let path: CGMutablePathRef = CGPathCreateMutable() CGPathAddRect(path, nil, textRect) let framesetter = CTFramesetterCreateWithAttributedString(self.attributedText) let tempFrame: CTFrameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, stringLength), path, nil) if (CFArrayGetCount(CTFrameGetLines(tempFrame)) == 0) { return CGRectZero } let lines: CFArrayRef = CTFrameGetLines(tempFrame) let numberOfLines: Int = self.numberOfLines > 0 ? min(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines) if (numberOfLines == 0) { return CGRectZero } var returnRect: CGRect = CGRectZero let nsLinesArray: NSArray = CTFrameGetLines(tempFrame) // Use NSArray to bridge to Array let ctLinesArray = nsLinesArray as Array var lineOriginsArray = [CGPoint](count:ctLinesArray.count, repeatedValue: CGPointZero) CTFrameGetLineOrigins(tempFrame, CFRangeMake(0, numberOfLines), &lineOriginsArray) for (var lineIndex: CFIndex = 0; lineIndex < numberOfLines; lineIndex++) { let lineOrigin: CGPoint = lineOriginsArray[lineIndex] let line: CTLineRef = unsafeBitCast(CFArrayGetValueAtIndex(lines, lineIndex), CTLineRef.self) //CFArrayGetValueAtIndex(lines, lineIndex) let lineRange: CFRange = CTLineGetStringRange(line) if ((lineRange.location <= substringRange.location) && (lineRange.location + lineRange.length >= substringRange.location + substringRange.length)) { var charIndex: CFIndex = substringRange.location - lineRange.location; // That's the relative location of the line var secondary: CGFloat = 0.0 let xOffset: CGFloat = CTLineGetOffsetForStringIndex(line, charIndex, &secondary); // Get bounding information of line var ascent: CGFloat = 0.0 var descent: CGFloat = 0.0 var leading: CGFloat = 0.0 let width: Double = CTLineGetTypographicBounds(line, &ascent, &descent, &leading) let yMin: CGFloat = floor(lineOrigin.y - descent); let yMax: CGFloat = ceil(lineOrigin.y + ascent); let yOffset: CGFloat = ((yMax - yMin) * CGFloat(lineIndex)) returnRect = CalculationHelper.sizeOfString(substring, font: self.font, width: DBL_MAX, height: DBL_MAX) returnRect.origin.x = xOffset + self.frame.origin.x returnRect.origin.y = yOffset + self.frame.origin.y + ((self.frame.size.height - textRect.size.height) / 2) break } } return returnRect