ios 如何在 UILabel 中找到文本子字符串的 CGRect?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/19417776/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 02:23:22  来源:igfitidea点击:

How do I locate the CGRect for a substring of text in a UILabel?

iosios7uilabeltextkit

提问by bryanjclark

For a given NSRange, I'd like to find a CGRectin a UILabelthat corresponds to the glyphs of that NSRange. For example, I'd like to find the CGRectthat contains the word "dog" in the sentence "The quick brown fox jumps over the lazy dog."

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

Visual description of problem

问题的视觉描述

The trick is, the UILabelhas multiple lines, and the text is really attributedText, so it's a bit tough to find the exact position of the string.

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

The method that I'd like to write on my UILabelsubclass would look something like this:

我想在我的UILabel子类上编写的方法如下所示:

 - (CGRect)rectForSubstringWithRange:(NSRange)range;


Details, for those who are interested:

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

My goal with this is to be able to create a new UILabel with the exact appearance and position of the UILabel, that I can then animate.I've got the rest figured out, but it's this step in particular that's holding me back at the moment.

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

What I've done to try and solve the issue so far:

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

  • I'd hoped that with iOS 7, there'd be a bit of Text Kit that would solve this problem, but most every example I've seen with Text Kit focuses on UITextViewand UITextField, rather than UILabel.
  • I've seen another question on Stack Overflow here that promises to solve the problem, but the accepted answer is over two years old, and the code doesn't perform well with attributed text.
  • 我希望在 iOS 7 中,会有一些 Text Kit 可以解决这个问题,但是我在 Text Kit 中看到的大多数示例都集中在UITextViewand 上UITextField,而不是UILabel.
  • 我在这里看到了关于 Stack Overflow 的另一个问题,它有望解决这个问题,但接受的答案已经超过两年了,并且代码在属性文本上表现不佳。

I'd bet that the right answer to this involves one of the following:

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

  • Using a standard Text Kit method to solve this problem in a single line of code. I'd bet it would involve NSLayoutManagerand textContainerForGlyphAtIndex:effectiveRange
  • Writing a complex method that breaks the UILabel into lines, and finds the rect of a glyph within a line, likely using Core Text methods. My current best bet is to take apart @mattt'sexcellent TTTAttributedLabel, which has a method that finds a glyph at a point - if I invert that, and find the point for a glyph, that might work.
  • 使用标准的 Text Kit 方法在一行代码中解决这个问题。我敢打赌它会涉及NSLayoutManagertextContainerForGlyphAtIndex:effectiveRange
  • 编写一个复杂的方法,将 UILabel 分成几行,并在一行中查找字形的矩形,可能使用 Core Text 方法。我目前最好的选择是拆开@mattt优秀的TTTAttributedLabel,它有一个方法可以在某个点找到一个字形 - 如果我反转它,并找到一个字形的点,那可能会奏效。


Update: Here's a github gist with the three things I've tried so far to solve this issue: https://gist.github.com/bryanjclark/7036101

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

回答by Luke Rogers

Following Joshua's answerin code, I came up with the following which seems to work well:

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

- (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];
}

回答by NoodleOfDeath

Building off of Luke Rogers's answerbut written in swift:

建立在卢克·罗杰斯的回答基础上,但写得很快:

Swift 2

斯威夫特 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)        
    }
}

Example Usage (Swift 2)

示例用法(Swift 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)

Swift 3/4

斯威夫特 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)       
    }
}

Example Usage (Swift 3/4)

示例用法 (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)

回答by Joshua

My suggestion would be to make use of Text Kit. Unfortunately we don't have access to the layout manager that a UILabeluses however it might be possible to create a replica of it and use that to get the rect for a range.

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

My suggestion would be to create a NSTextStorageobject containing the exact same attributed text as is in your label. Next create a NSLayoutManagerand add that to the the text storage object. Finally create a NSTextContainerwith the same size as the label and add that to the layout manager.

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

Now the text storage has the same text as the label and the text container is the same size as the label so we should be able to ask the layout manager we created for a rect for our range using boundingRectForGlyphRange:inTextContainer:. Make sure you convert your character range to a glyph range first using glyphRangeForCharacterRange:actualCharacterRange:on the layout manager object.

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

All going well that should give you a boundingCGRectof the range you specified within the label.

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

I haven't tested this but this would be my approach and by mimicking how the UILabelitself works should have a good chance of succeeding.

我还没有测试过这个,但这将是我的方法,通过模仿UILabel它本身的工作方式应该有很大的成功机会。

回答by Dima

swift 4 solution, will work even for multiline strings, bounds were replaced with intrinsicContentSize

swift 4 解决方案,甚至适用于多行字符串,边界被替换为内在内容大小

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

回答by Max O

The proper answer is you actually can't. Most of the solutions here trying to re-create UILabelinternal behavior with varying precision. It just so happens that in recent iOS versions UILabelrenders text using TextKit/CoreText frameworks. In earlier versions it actually used WebKit under the hood. Who knows what it will use in future. Reproducing with TextKit have obvious problems even right now (not talking about future-proof solutions). We don't quite know (or can't guarantee) how the text layout and rendering are actually performed, i.e. which parameters are used – just look at all the edge cases mentioned. Some solution may 'accidentally' work for you for some simple cases you tested – that doesn't guarantee anything even within current version of iOS. Text rendering is hard, don't get me started on RTL support, Dynamic Type, emojis etc.

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

If you need a proper solution just don't use UILabel. It is simple component and the fact that its TextKit internals aren't exposed in the API is a clear indication that Apple don't want developers to build solutions relying on this implementation detail.

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

Alternatively you can use UITextView. It conveniently exposes it's TextKit internals and is very configurable so you can achieve almost everything UILabelis good for.

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

You may also just go lower level and use TextKit directly (or even lower with CoreText). Chances are, if you actually need to find text positions you may soon need other powerful tools available in these frameworks. They are rather hard to use at first but I feel like most of the complexity comes from actually learning how text layout and rendering works – and that's a very relevant and useful skill. Once you get familiar with Apple's excellent text frameworks you will feel empowered to do so much more with text.

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

回答by pickwick

Can you instead base your class on UITextView? If so, check out the UiTextInput protocol methods. See in particular the geometry and hit resting methods.

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

回答by Hemang

For anyone who's looking for a plain textextension!

对于任何正在寻找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)
    }
}

P.S. Updated answer of Noodle of Death`sanswer.

PS 更新了死亡面条的答案

回答by freshking

Another way of doing this, if you have automatic font size adjustment enabled, would be like this:

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

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