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


78

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

问题的视觉描述

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

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

 - (CGRect)rectForSubstringWithRange:(NSRange)range;

细节,对于那些感兴趣的人:

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

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

  • 我希望在iOS 7上有一些Text Kit可以解决此问题,但是我在Text Kit上看到的每个示例大多数都针对UITextViewUITextField,而不是UILabel
  • 我在这里看到了有关Stack Overflow的另一个问题,该问题有望解决该问题,但是公认的答案已经使用了两年多了,并且代码在归因于文本的情况下表现不佳。

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

  • 使用标准的文本工具包方法在一行代码中解决此问题。我敢打赌这会涉及NSLayoutManagertextContainerForGlyphAtIndex:effectiveRange
  • 编写一个复杂的方法,将UILabel分成几行,并可能使用Core Text方法在一行中找到一个字形的矩形。我目前最好的选择是分解@mattt出色的TTTAttributedLabel,该方法具有一种在某个点找到字形的方法-如果我将其反转,然后找到一个字形的点,则可能会起作用。

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


该死 nslabeloutmanager为什么不在uilabel上公开。试图解决同一件事。您有解决方案吗?
Bob Spryn

还没有回到这个问题上,但是经过大约六次不同的尝试之后,我最大的希望是实现类似于约书亚下面的建议。我将分享我的想法。
bryanjclark

1
也许不是您需要的100%,但请查看github.com/SebastienThiebaud/STTweetLabel。他利用textview进行存储和计算。添加参考所需的内容应该非常简单。
Bob Spryn

谢谢,鲍勃!我曾经考虑过使用STTweetLabel,但最后在TTTAttributedLabel上创建了自己的自定义变体-我将研究STTweetLabel,看看他们如何解决了这个问题。听起来像是在正确的轨道上。
bryanjclark

1
Luke的代码始终有效。但是有时返回的rect为零。经过长时间解决错误的结果,我发现最好使用UITextView,使用UITextView的textContainer和layoutManager,结果更有价值。我认为UILabel的内部textContainer与Luke的代码生成的文本之间可能存在一些差异。
Dan Lee

Answers:


92

遵循约书亚在代码中的回答,我提出了以下似乎很好的方法:

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

9
很酷!但是,人们应该意识到,如果字体已自动缩小以适合当前视图的大小,则此方法将无法正常工作。您需要相应地调整字体大小。
arsenius

这段代码在iOS7上运行良好。有人在iOS6上尝试过吗,在我的代码中,它在iOS6上崩溃,并显示日志[__NSCFType _isDefaultFace]:无法识别的选择器已发送到实例。有人对此有任何想法吗?
Richa 2014年

3
好答案。由于UILabel没有左页边距,因此请确保添加textContainer.lineFragmentPadding = 0;
colinbrash 2014年

我正在使用它,效果很好,但不适用于我通过的每个范围。请参阅我的SO帖子,这里有更详细的描述。
斯蒂芬

2
太棒了,但是该解决方案没有考虑attributedText的对齐方式。
加布里埃尔。马萨纳2015年

20

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

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

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

迅捷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)

1
我用以下方法解决了崩溃问题:var glyphRange = NSRange() layoutManager.characterRangeForGlyphRange(range, actualGlyphRange: &glyphRange)
Sonia Casas

1
@Nemanja你如何打破线距?我希望它像这样imgur.com/a/P9RzR,但是当文本接近尾声时,它将不起作用。
demiculus

4
重要的是要注意,UILabel的属性文本必须具有键NSAttributedStringKey.paragraphStyle和NSAttributedStringKey.font的属性集,以便此函数可以说明该文本对齐方式和字体
Zigglzworth

3
@Hemang用法示例:let t = "aa bb cc"; label.attributedText = NSAttributedString(string: t); let l = CALayer(); l.borderWidth = 1; l.frame = label.boundingRectForCharacterRange(NSRange(t.range(of: "bb")!, in: t)); label.layer.addSublayer(l);
心教堂

1
如果您发现它不适用于居中文本,请不要使用标签的对齐方式,将属性字符串居中...
Ixx,

13

我的建议是利用文本工具包。不幸的是,我们无法UILabel使用a的布局管理器,但是可以创建它的副本并使用该副本来获取范围的rect。

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

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

一切顺利,应该给您一个界限 CGRect你的标签中指定的范围。

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


这听起来像是个不错的选择-我会试一试并稍后报告!如果可行,我将在GitHub上为我工作的代码。谢谢!
bryanjclark

@bryanjclark你有运气吗?
2013年

我正是这样做的,但是它确实有效,但并非完美,我的偏移量很小,因此它无法正确检测到所触摸的单词,并且在触摸右侧的单词时也可以使用...
Andres

5

迅捷的4解决方案,即使对于多行字符串也可以使用,边界已替换为internalContentSize

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

不幸的是,这根本不起作用:/它只是提供了internalContentSize(“总体印刷尺寸”,而不是实际的字形形状)。您可以在此处查看结果: stackoverflow.com/questions/56935898
Fattie

1

您可以改用基于UITextView的类吗?如果是这样,请检查UiTextInput协议方法。尤其参见几何形状和击中停止方法。


不幸的是,我认为这不是务实的选择。我的UILabel是对TTTAttributedLabel的自定义重写,如果可能的话,我不愿意重写大量代码来创建此方法。不过谢谢你!
bryanjclark

@bryanjclark您是否找到解决方案?
Jenel Ejercito Myers

1

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

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

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

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


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更新了《死亡面条》的 答案


.attributedText与.text兼容:您将获得相同的CGRect。
心教堂

不幸的是,这根本行不通。您可以在此处看到输出:stackoverflow.com/questions/56935898
Fattie

-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

1
什么是“ CalculationHelper”?
卡梅洛·加洛

那只是我写的用于计算rect的一
门课

2
是的,我知道这是一堂课:)您能分享一下吗?否则,您的代码将不完整;)
卡梅洛·加洛

2
我的意思不是boundingRectWithSize:但是如果您可以共享CalculationHelper类。无论如何,我已经自己弄清楚了;)
卡梅洛·加洛

@freshking到boundingRectWithSize的链接现在应该是developer.apple.com/documentation/foundation/nsstring/…。为了避免代码不完整,我直接将它包含在您的答案中。
心教堂
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.