【问题标题】:How do I locate the CGRect for a substring of text in a UILabel?如何在 UILabel 中找到文本子字符串的 CGRect?
【发布时间】:2013-10-25 09:57:45
【问题描述】:

对于给定的NSRange,我想在UILabel 中找到与NSRange 的字形相对应的CGRect。例如,我想在“The quick brown fox jumps over the lazy dog”这句话中找到包含“dog”一词的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's 优秀的 TTTAttributedLabel,它有一种方法可以在某个点找到一个字形 - 如果我将其反转,并找到一个字形的点,那可能会奏效。

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

【问题讨论】:

  • 该死的。为什么 nslayoutmanager 没有暴露在 uilabel 上。试图解决同样的问题。你想出解决方案了吗?
  • 还没有回到这个问题上,但是经过大约六次不同的尝试,我最大的希望是实现类似于下面约书亚建议的东西。当我这样做时,我会分享我的发现!
  • 也许不是 100% 您需要的,但请查看 github.com/SebastienThiebaud/STTweetLabel。他利用文本视图进行存储和计算。添加您需要的内容作为参考应该相当简单。
  • 谢谢,鲍勃!我曾经考虑过使用 STTweetLabel,但最终在 TTTAttributedLabel 上创建了我自己的自定义变体——我将研究 STTweetLabel,看看他们是如何解决这个问题的。听起来这是在正确的轨道上。
  • Luke 的代码在大多数情况下都能正常工作。但有时返回的矩形为零。解决了好久的错误结果,发现还是用UITextView比较好,用UITextView的textContainer和layoutManager,结果更珍贵。我认为 UILabel 的内部 textContainer 和 Luke 的代码生成的可能有一些区别。

标签: ios ios7 uilabel textkit


【解决方案1】:

我不热衷于将范围作为参数提供,因为我很懒。我宁愿让方法为我找到范围。考虑到这一点,我对上面的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)
    }
}

【讨论】:

    【解决方案2】:

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

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

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

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

    【讨论】:

      【解决方案3】:

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

      【讨论】:

      • 不幸的是,这根本行不通:/它只是给出了intrinsicContentSize(“整体印刷尺寸”,而不是实际的字形形状)。你可以在这里看到结果:stackoverflow.com/questions/56935898
      【解决方案4】:

      Luke Rogers's answer 为基础,但用 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)
      

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

      【讨论】:

      • 我用这个解决了崩溃:var glyphRange = NSRange() layoutManager.characterRangeForGlyphRange(range, actualGlyphRange: &glyphRange)
      • @Nemanja 你如何打破行与行之间的范围?我希望它像这样 imgur.com/a/P9RzR 但是当文本接近结尾时它不起作用。
      • 请务必注意,您的 UILabel 的属性文本必须具有键 NSAttributedStringKey.paragraphStyle 和 NSAttributedStringKey.font 的属性,以便此函数可以考虑文本对齐和字体
      • @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);
      • 如果您发现它不适用于居中文本,请不要使用标签对齐方式,将属性字符串居中...
      【解决方案5】:

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

      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
      

      【讨论】:

      • 什么是“CalculationHelper”?
      • 这只是我写的一个计算字符串rect的类
      • 是的,我知道这是一门课 :) 你能分享一下吗?否则你的代码不完整;)
      • 我不是说 boundingRectWithSize: 但如果你可以分享 CalculationHelper 类。无论如何,我自己想通了;)
      • @freshking 到 boundingRectWithSize 的链接现在应该是 developer.apple.com/documentation/foundation/nsstring/…。我已将您的答案直接包含在内,以避免代码不完整。
      【解决方案6】:

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

      附注Noodle of Death`s answer的更新答案。

      【讨论】:

      【解决方案7】:

      在代码中遵循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];
      }
      

      【讨论】:

      • 非常酷!但是,人们应该知道,如果字体已自动缩小以适合当前视图大小,则这将无法正常工作。您需要相应地调整字体大小。
      • 此代码在 iOS7 上运行良好。有没有人在 iOS6 上尝试过,在我的代码中它在 iOS6 上崩溃,日志 [__NSCFType _isDefaultFace]: unrecognized selector sent to instance.有人对此有任何想法吗?
      • 很好的答案。由于 UILabel 没有左边距,请确保添加 textContainer.lineFragmentPadding = 0;
      • 我正在使用它并且效果很好,但不适用于我通过的每个范围。 See my SO post here describing in more detail.
      • 太棒了,但是解决方案没有考虑到属性文本的对齐方式。
      【解决方案8】:

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

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

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

      一切顺利,您应该在标签内指定范围的边界 CGRect

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

      【讨论】:

      • 这听起来不错 - 我会试一试,稍后再报告!如果成功,我将在 GitHub 上提供最终为我工作的代码。谢谢!
      • @bryanjclark 你运气好吗?
      • 我确实做到了这一点,它可以工作,但并不完美,我有一点偏移,所以它无法正确检测到触摸的单词,并且在触摸正确的单词时它可以工作边...
      【解决方案9】:

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

      【讨论】:

      • 不幸的是,我认为这不是实用的选择。我的 UILabel 是对 TTTAttributedLabel 的自定义重写,如果可能的话,我不希望不得不重写大量代码来创建这种方法。不过,谢谢!
      • @bryanjclark 你有没有找到解决方案?
      猜你喜欢
      • 2011-10-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-09-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多