对于给定的NSRange
,我想在CGRect
中找到与UILabel
的字形对应的NSRange
。例如,我想在“快速的棕色狐狸跳过懒狗”这句话中找到包含“狗”字样的CGRect
。
诀窍是,UILabel
有多行,文本真的是attributedText
,所以要找到字符串的确切位置有点困难。
我想在我的UILabel
子类上写的方法看起来像这样:
- (CGRect)rectForSubstringWithRange:(NSRange)range;
详细信息,对于那些感兴趣的人:
我的目标是能够创建一个具有UILabel的确切外观和位置的新UILabel,然后我可以制作动画。我已经找到了其余部分,但它是特别是这一步让我暂时退缩。
到目前为止我尝试解决问题的方法是什么:
UITextView
和{{1} },而不是UITextField
。我敢打赌,对此的正确答案涉及以下其中一项:
UILabel
和NSLayoutManager
更新:这是一个github要点,其中包含我迄今为止尝试解决此问题的三件事:https://gist.github.com/bryanjclark/7036101
答案 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