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

时间:2013-10-17 03:31:54

标签: ios ios7 uilabel textkit

对于给定的NSRange,我想在CGRect中找到与UILabel的字形对应的NSRange。例如,我想在“快速的棕色狐狸跳过懒狗”这句话中找到包含“狗”字样的CGRect

Visual description of problem

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

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

 - (CGRect)rectForSubstringWithRange:(NSRange)range;

详细信息,对于那些感兴趣的人:

我的目标是能够创建一个具有UILabel的确切外观和位置的新UILabel,然后我可以制作动画。我已经找到了其余部分,但它是特别是这一步让我暂时退缩。

到目前为止我尝试解决问题的方法是什么:

  • 我希望在iOS 7中有一些Text Kit可以解决这个问题,但是我在Text Kit中看到的大多数例子都集中在UITextView和{{1} },而不是UITextField
  • 我在这里看到了有关Stack Overflow的另一个问题,它承诺可以解决问题,但是已接受的答案已经超过两年了,并且代码在属性文本中表现不佳。

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

  • 使用标准文本工具包方法在一行代码中解决此问题。我敢打赌它会涉及UILabelNSLayoutManager
  • 编写一个复杂的方法,将UILabel分成几行,并在一行中找到一个字形的矩形,可能使用Core Text方法。我目前最好的办法是拆开@mattt's优秀的TTTAttributedLabel,它有一个方法可以在一个点上找到一个字形 - 如果我将其反转,并找到一个可能有效的字形点。 / LI>

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

10 个答案:

答案 0 :(得分:87)

在代码中关注Joshua's answer后,我想出了以下哪些内容似乎运行良好:

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

答案 1 :(得分:16)

建立Luke Rogers's answer,但用swift编写:

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

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

答案 2 :(得分:11)

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

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

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

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

我没有对此进行测试,但这将是我的方法,并通过模仿UILabel本身的工作方式应该很有可能取得成功。

答案 3 :(得分:3)

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

答案 4 :(得分:2)

翻译为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)
    }

答案 5 :(得分:1)

您可以将您的课程基于UITextView吗?如果是这样,请查看UiTextInput协议方法。特别参见几何和击中静止方法。

答案 6 :(得分: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)
    }
}

<子> P.S。更新了 Noodle of Death`s answer的答案。

答案 7 :(得分:0)

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

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

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

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

答案 8 :(得分:0)

我不喜欢将范围作为参数提供,因为我很懒。我宁愿让方法为我找到范围。考虑到这一点,我修改了上面的 NoodleOfDeath's answer 以将 String 作为参数。

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

答案 9 :(得分:-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