Как найти CGRect для подстроки текста в UILabel?
Для данного NSRange
я хотел бы найти CGRect
в UILabel
, который соответствует глифам этого NSRange
. Например, я хотел бы найти CGRect
, который содержит слово "собака" в предложении "Быстрая коричневая лиса прыгает через ленивую собаку".
![Visual description of problem]()
Трюк состоит в том, что UILabel
имеет несколько строк, а текст действительно attributedText
, поэтому немного сложно найти точное положение строки.
Метод, который я хотел бы написать в моем подклассе UILabel
, выглядел бы примерно так:
- (CGRect)rectForSubstringWithRange:(NSRange)range;
Подробности для тех, кто заинтересован:
Моя цель состоит в том, чтобы создать новый UILabel с точным внешним видом и позицией UILabel, который я смогу затем оживить. Я все понял, но это, в частности, этот шаг, который удерживает меня в данный момент.
Что я сделал, чтобы решить проблему до сих пор:
- Я надеялся, что с iOS 7 будет немного текстового набора, который бы разрешил эту проблему, но в большинстве случаев, которые я видел с помощью Text Kit, основное внимание уделяется
UITextView
и UITextField
, а не UILabel
.
- Я видел еще один вопрос о переполнении стека здесь, чтобы promises решить проблему, но принятый ответ более двух лет, и код не работает хорошо с атрибутом текста.
Готов поспорить, что правильный ответ на это включает в себя одно из следующего:
- Использование стандартного метода Text Kit для решения этой проблемы в одной строке кода. Я бы поспорил, что он будет включать
NSLayoutManager
и textContainerForGlyphAtIndex:effectiveRange
- Написание сложного метода, который разбивает UILabel на строки и находит прямой глифа внутри строки, вероятно, используя методы Core Text. Моя лучшая ставка заключается в том, чтобы отделить @mattt's отличный TTTAttributedLabel, который метод, который находит глиф в точке - если я инвертирую это и найду точку для глифа, которая может работать.
Обновление: здесь github gist с тремя вещами, которые я до сих пор пытался решить эту проблему: https://gist.github.com/bryanjclark/7036101
Ответы
Ответ 1
Следуя Joshua 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];
}
Ответ 2
Строительство Луки Роджерса отвечает, но написано быстро:
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)
Ответ 3
Мое предложение - использовать Text Kit. К сожалению, у нас нет доступа к диспетчеру макета, который использует UILabel
, однако возможно создать реплику и использовать его для получения прямоугольника для диапазона.
Мое предложение состояло в том, чтобы создать объект NSTextStorage
, содержащий тот же самый атрибут, что и в вашей метке. Затем создайте NSLayoutManager
и добавьте это в объект хранения текста. Наконец, создайте NSTextContainer
с тем же размером, что и метка, и добавьте его в менеджер компоновки.
Теперь текстовое хранилище имеет тот же текст, что и метка, а текстовый контейнер имеет тот же размер, что и ярлык, поэтому мы можем попросить менеджера макетов, который мы создали для прямоугольника для нашего диапазона, используя boundingRectForGlyphRange:inTextContainer:
. Убедитесь, что вы сначала конвертируете свой диапазон символов в диапазон глифов, используя glyphRangeForCharacterRange:actualCharacterRange:
в объекте менеджера макета.
Все идет хорошо, что должно дать вам ограничивающий CGRect
диапазон, указанный в метке.
Я не тестировал это, но это был бы мой подход и подражание тому, как работает сам UILabel
, должен иметь хорошие шансы на успех.
Ответ 4
Переведено для 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
Можете ли вы вместо этого создать свой класс в UITextView? Если это так, проверьте методы протокола UiTextInput. См., В частности, геометрию и методы отдыха.
Ответ 6
К сожалению, в UILabel нет прямого API. И чтобы точно поддерживать такую функцию, вам нужно много работать, имея в виду разные символы. Однако кто-то уже потратил время на это. Пожалуйста, взгляните на приведенный ниже код и посмотрите, работает ли это для вас:
UILabel получает CGRect для подстроки текста
Ответ 7
Для тех, кто ищет 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 Обновленного ответа лапши из Мертвой ответа.
Ответ 8
быстрое решение 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)
}
}
Ответ 9
Другой способ сделать это, если вы включили автоматическую настройку размера шрифта, будет выглядеть так:
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 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