78

对于给定的NSRange,我想CGRect在 a中找到UILabel与 that 的字形相对应的a NSRange。例如,我想CGRect在“The quick brown fox jumps over the lazy dog”这句话中找到包含“dog”这个词的那个。

问题的视觉描述

诀窍是,UILabel有多行,而文本真的是attributedText,所以要找到字符串的确切位置有点困难。

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

 - (CGRect)rectForSubstringWithRange:(NSRange)range;

详情,有兴趣的朋友可以看看:

我的目标是能够创建一个具有 UILabel 确切外观和位置的新 UILabel,然后我可以对其进行动画处理。我已经弄清楚了其余的部分,但目前正是这一步阻碍了我。

到目前为止,我为尝试解决该问题所做的工作:

  • 我希望在 iOS 7 中会有一些 Text Kit 可以解决这个问题,但我看到的大多数 Text Kit 示例都集中在UITextViewand上UITextField,而不是UILabel.
  • 我在这里看到了关于 Stack Overflow 的另一个问题,该问题有望解决该问题,但接受的答案已有两年多的历史,并且代码在属性文本中表现不佳。

我敢打赌,这个问题的正确答案涉及以下之一:

  • 使用标准的 Text Kit 方法在一行代码中解决此问题。我敢打赌它会涉及NSLayoutManagertextContainerForGlyphAtIndex:effectiveRange
  • 编写一个复杂的方法,将 UILabel 分成几行,并在一行中找到字形的矩形,可能使用 Core Text 方法。我目前最好的选择是拆开@mattt 的优秀TTTAttributedLabel,它有一种方法可以在某个点找到一个字形 - 如果我将其反转并找到一个字形的点,那可能会起作用。

更新:这是一个 github 要点,其中包含我迄今为止尝试解决此问题的三件事:https ://gist.github.com/bryanjclark/7036101

4

9 回答 9

95

按照约书亚在代码中的回答,我想出了以下似乎效果很好的方法:

- (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];
}
于 2013-12-17T11:52:39.453 回答
22

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

斯威夫特 2

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)        
    }
}

示例用法(斯威夫特 2)

let label = UILabel()
let text = "aa bb cc"
label.attributedText = NSAttributedString(string: text)
let sublayer = CALayer()
sublayer.borderWidth = 1
sublayer.frame = label.boundingRectForCharacterRange(NSRange(text.range(of: "bb")!, in: text))
label.layer.addSublayer(sublayer)

斯威夫特 3/4

extension UILabel {
    func boundingRect(forCharacterRange 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)       
    }
}

示例用法(Swift 3/4)

let label = UILabel()
let text = "aa bb cc"
label.attributedText = NSAttributedString(string: text)
let sublayer = CALayer()
sublayer.borderWidth = 1
sublayer.frame = label.boundingRect(forCharacterRange: NSRange(text.range(of: "bb")!, in: text))
label.layer.addSublayer(sublayer)
于 2016-06-07T21:51:27.237 回答
13

我的建议是使用 Text Kit。不幸的是,我们无法访问 aUILabel使用的布局管理器,但是可以创建它的副本并使用它来获取范围的矩形。

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

现在文本存储具有与标签相同的文本,并且文本容器与标签的大小相同,因此我们应该能够向我们创建的布局管理器询问使用boundingRectForGlyphRange:inTextContainer:. 确保首先使用glyphRangeForCharacterRange:actualCharacterRange:布局管理器对象将字符范围转换为字形范围。

一切顺利,应该为您在标签中指定的范围提供一个边界。 CGRect

我还没有对此进行测试,但这将是我的方法,并且通过模仿其UILabel本身的工作方式应该有很大的成功机会。

于 2013-10-27T16:42:41.633 回答
5

swift 4 解决方案,甚至适用于多行字符串,边界已替换为intrinsicContentSize

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: intrinsicContentSize)

    textContainer.lineFragmentPadding = 0.0

    layoutManager.addTextContainer(textContainer)

    var glyphRange = NSRange()

    layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)

    return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
}
}
于 2018-12-03T16:12:27.907 回答
1

你能把你的类建立在 UITextView 上吗?如果是这样,请查看 UiTextInput 协议方法。特别参见几何和击球休息方法。

于 2013-10-17T04:31:37.887 回答
1

正确的答案是你实际上不能。UILabel这里的大多数解决方案都试图以不同的精度重新创建内部行为。恰好在最近的 iOS 版本中UILabel使用 TextKit/CoreText 框架呈现文本。在早期版本中,它实际上在底层使用了 WebKit。谁知道它将来会用什么。即使是现在,使用 TextKit 进行复制也存在明显的问题(不是在谈论面向未来的解决方案)。我们不太清楚(或不能保证)文本布局和渲染是如何实际执行的,即使用了哪些参数——只要看看提到的所有边缘情况。对于您测试的一些简单案例,某些解决方案可能会“意外地”为您工作——即使在当前版本的 iOS 中也不能保证任何事情。文本渲染很难,不要让我开始 RTL 支持、动态类型、表情符号等。

如果您需要适当的解决方案,请不要使用UILabel. 它是一个简单的组件,而且它的 TextKit 内部没有在 API 中公开这一事实清楚地表明 Apple 不希望开发人员构建依赖于这个实现细节的解决方案。

或者,您可以使用 UITextView。它方便地公开了它的 TextKit 内部结构,并且非常可配置,因此您几乎可以实现所有有用的东西UILabel

你也可以直接使用 TextKit (或者使用 CoreText 甚至更低)。很有可能,如果您确实需要查找文本位置,您可能很快就会需要这些框架中提供的其他强大工具。一开始它们很难使用,但我觉得大部分复杂性来自于实际学习文本布局和渲染的工作原理——这是一项非常相关和有用的技能。一旦您熟悉了 Apple 出色的文本框架,您就会感到自己有权在文本方面做更多事情。

于 2019-01-07T15:56:07.720 回答
0

对于任何正在寻找plain text扩展的人!

extension UILabel {    
    func boundingRectForCharacterRange(range: NSRange) -> CGRect? {
        guard let text = text else { return nil }

        let textStorage = NSTextStorage.init(string: text)
        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)
    }
}

PS 更新了死亡面条的 答案

于 2018-03-12T11:35:12.527 回答
0

我不热衷于将范围作为参数提供,因为我很懒。我宁愿让方法为我找到范围。考虑到这一点,我在上面改编了 NoodleOfDeath 的答案以将 aString作为参数。

extension String {
    func nsRange(of substring: String) -> NSRange? {
        // Get the swift range
        guard let range = range(of: substring) else { return nil }

        // Get the distance to the start of the substring
        let start = distance(from: startIndex, to: range.lowerBound) as Int
        //Get the distance to the end of the substring
        let end = distance(from: startIndex, to: range.upperBound) as Int

        //length = endOfSubstring - startOfSubstring
        //start = startOfSubstring
        return NSMakeRange(start, end - start)
    }
}


extension UILabel {
    /// Finds the bounding rect of a substring in a `UILabel`
    /// - Parameter substring: The substring whose bounding box you'd like to determine
    /// - Returns: An optional `CGRect`
    func boundingRect(for substring: String) -> CGRect? {
        guard let attributedText = attributedText,
              let text = self.text,
              let range: NSRange = text.nsRange(of: substring) 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()
        layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)

        return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
    }
}
于 2021-07-07T18:26:05.993 回答
-1

另一种方法是,如果您启用了自动字体大小调整,则如下所示:

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 = (substring as NSString).boundingRect(with: CGSize(width: Double.greatestFiniteMagnitude, height: Double.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, attributes: [.font: self.font ?? UIFont.systemFont(ofSize: 1)], context: nil)
        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
于 2015-02-17T10:16:50.127 回答