For a given NSRange
, I'd like to find a CGRect
in a UILabel
that corresponds to the glyphs of that NSRange
. For example, I'd like to find the CGRect
that contains the word "dog" in the sentence "The quick brown fox jumps over the lazy dog."
对于给定的NSRange,我想在UILabel中找到一个CGRect,它对应于NSRange的符号。例如,我想在“敏捷的棕色狐狸跳过懒惰的狗”这句话中找到包含单词“dog”的CGRect。
The trick is, the UILabel
has 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 UILabel
subclass 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
UITextView
andUITextField
, rather thanUILabel
. - 我希望iOS 7中有一些文本工具包可以解决这个问题,但是我看到的大多数文本工具包都关注UITextView和UITextField,而不是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.
- 我在这里看到了另一个关于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
NSLayoutManager
andtextContainerForGlyphAtIndex:effectiveRange
- 使用标准的文本工具包方法在一行代码中解决这个问题。我敢打赌它会包括NSLayoutManager和textContainerForGlyphAtIndex: efficient verange
- 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's excellent 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.
- 编写一个复杂的方法,将UILabel分解为行,并在一行中找到字形的矩形,很可能使用核心文本方法。我目前的最佳选择是拆开@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
7 个解决方案
#1
80
Following Joshua's answer in code, I came up with the following which seems to work well:
根据约书亚的密码,我想到了以下似乎很有效的答案:
- (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];
}
#2
15
Building off of Luke Rogers's answer but written in swift:
以卢克·罗杰斯(Luke Rogers)的回答为基础,但以斯威夫特(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)
示例使用(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
斯威夫特3
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)
范例用法(斯威夫特3)
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)
#3
10
My suggestion would be to make use of Text Kit. Unfortunately we don't have access to the layout manager that a UILabel
uses however it might be possible to create a replica of it and use that to get the rect for a range.
我的建议是使用Text Kit。不幸的是,我们无法访问UILabel使用的布局管理器,但是可能可以创建它的副本并使用它来获取范围的rect。
My suggestion would be to create a NSTextStorage
object containing the exact same attributed text as is in your label. Next create a NSLayoutManager
and add that to the the text storage object. Finally create a NSTextContainer
with 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:在layout manager对象上,将您的字符范围转换为字形范围。
All going well that should give you a bounding CGRect
of the range you specified within the label.
一切都进行得很顺利,这将为您提供您在标签中指定的范围的边界CGRect。
I haven't tested this but this would be my approach and by mimicking how the UILabel
itself works should have a good chance of succeeding.
我还没有对此进行测试,但这将是我的方法,通过模仿UILabel本身的工作应该有一个很好的成功机会。
#4
2
Translated for Swift 3.
翻译迅速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
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协议方法。具体看几何和打击休息方法。
#6
0
For anyone who's looking for a plain text
extension!
对于任何正在寻找纯文本扩展的人来说!
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`s answer.
P.S.更新了死亡的答案。
#7
-1
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
#1
80
Following Joshua's answer in code, I came up with the following which seems to work well:
根据约书亚的密码,我想到了以下似乎很有效的答案:
- (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];
}
#2
15
Building off of Luke Rogers's answer but written in swift:
以卢克·罗杰斯(Luke Rogers)的回答为基础,但以斯威夫特(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)
示例使用(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
斯威夫特3
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)
范例用法(斯威夫特3)
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)
#3
10
My suggestion would be to make use of Text Kit. Unfortunately we don't have access to the layout manager that a UILabel
uses however it might be possible to create a replica of it and use that to get the rect for a range.
我的建议是使用Text Kit。不幸的是,我们无法访问UILabel使用的布局管理器,但是可能可以创建它的副本并使用它来获取范围的rect。
My suggestion would be to create a NSTextStorage
object containing the exact same attributed text as is in your label. Next create a NSLayoutManager
and add that to the the text storage object. Finally create a NSTextContainer
with 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:在layout manager对象上,将您的字符范围转换为字形范围。
All going well that should give you a bounding CGRect
of the range you specified within the label.
一切都进行得很顺利,这将为您提供您在标签中指定的范围的边界CGRect。
I haven't tested this but this would be my approach and by mimicking how the UILabel
itself works should have a good chance of succeeding.
我还没有对此进行测试,但这将是我的方法,通过模仿UILabel本身的工作应该有一个很好的成功机会。
#4
2
Translated for Swift 3.
翻译迅速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
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协议方法。具体看几何和打击休息方法。
#6
0
For anyone who's looking for a plain text
extension!
对于任何正在寻找纯文本扩展的人来说!
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`s answer.
P.S.更新了死亡的答案。
#7
-1
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