Как найти CGRect для подстроки текста в UILabel?

Для данного NSRange я бы хотел найти CGRect в UILabel, который соответствует символам этого NSRange. Например, я хотел бы найти CGRect, который содержит слово «собака» в предложении «Быстрая коричневая лиса перепрыгивает через ленивая собака. "

Визуальное описание проблемы

Хитрость в том, что UILabel состоит из нескольких строк, и текст действительно attributedText, поэтому довольно сложно найти точное положение строки.

Метод, который я хотел бы написать в моем подклассе UILabel, будет выглядеть примерно так:

 - (CGRect)rectForSubstringWithRange:(NSRange)range;

Подробности для тех, кому это интересно:

Моя цель в этом заключается в том, чтобы иметь возможность создать новую UILabel с точным внешним видом и положением UILabel, которую я могу затем анимировать. Остальное я понял, но это этот шаг, в частности, удерживает меня на данный момент.

Что я сделал, чтобы попытаться решить проблему до сих пор:

  • Я надеялся, что в iOS 7 будет немного Text Kit, который решит эту проблему, но в большинстве случаев, которые я видел в Text Kit, основное внимание уделяется UITextView и UITextField, а не UILabel
  • Я видел еще один вопрос о переполнении стека, который обещает решить проблему, но принятый ответ уже более двух лет, и код не работает с атрибутивным текстом.

Держу пари, что правильный ответ на этот вопрос предполагает одно из следующих действий:

  • Использование стандартного метода Text Kit для решения этой проблемы в одной строке кода. Могу поспорить, что это будет связано с NSLayoutManager и textContainerForGlyphAtIndex:effectiveRange
  • Написание сложного метода, который разбивает UILabel на строки и находит прямоугольник глифа в строке, вероятно, с использованием методов Core Text. В настоящее время лучше всего разбирать @ mattt's превосходно TTTAttributedLabel , который имеет метод, который находит глиф в точке - если я инвертирую это и найду точку для глифа, это может сработать.

Обновление: вот краткое описание github с тремя вещами, которые я до сих пор пытался решить эту проблему: https : //gist.github.com/bryanjclark/7036101

67 голосов | спросил bryanjclark 17 +04002013-10-17T07:31:53+04:00312013bEurope/MoscowThu, 17 Oct 2013 07:31:53 +0400 2013, 07:31:53

9 ответов


0

Следуя ответу Джошуа в коде, я пришел к следующему, которое, похоже, работает хорошо:

- (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];
}
ответил Luke Rogers 17 TueEurope/Moscow2013-12-17T15:52:39+04:00Europe/Moscow12bEurope/MoscowTue, 17 Dec 2013 15:52:39 +0400 2013, 15:52:39
0

Строительство прочь Люка Роджерса ответ , но написано в стрижа:

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)
ответил NoodleOfDeath 8 J0000006Europe/Moscow 2016, 00:51:27
0

Я бы хотел использовать Text Kit. К сожалению, у нас нет доступа к диспетчеру компоновки, который использует UILabel, однако возможно создать его копию и использовать ее чтобы получить прямоугольник для диапазона.

Я бы предложил создать объект NSTextStorage, содержащий точно такой же атрибутный текст, как и в вашем ярлыке. Затем создайте NSLayoutManager и добавьте его к объекту хранения текста. Наконец, создайте NSTextContainer того же размера, что и метка, и добавьте его в менеджер макета.

Теперь текстовое хранилище имеет тот же текст, что и метка, а текстовый контейнер имеет тот же размер, что и метка, поэтому мы должны иметь возможность попросить менеджера макета, который мы создали, для прямоугольника для нашего диапазона, используя boundingRectForGlyphRange:inTextContainer:. Убедитесь, что вы сначала преобразовали свой диапазон символов в диапазон глифов, используя glyphRangeForCharacterRange:actualCharacterRange: на объекте менеджера макета.

Все идет хорошо, что должно дать вам ограничение CGRect диапазона, указанного в метке.

Я не проверял это, но это мой подход, и, имитируя, как работает сам UILabel, нужно иметь хорошие шансы на успех.

ответил Joshua 27 +04002013-10-27T20:42:41+04:00312013bEurope/MoscowSun, 27 Oct 2013 20:42:41 +0400 2013, 20:42:41
0

Переведено для 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)
    }
ответил Nico Achkasov 25 Mayam17 2017, 08:36:53
0

Можете ли вы вместо этого основывать свой класс на UITextView? Если так, проверьте методы протокола UiTextInput. Посмотрите, в частности, геометрию и методы покоя.

ответил pickwick 17 +04002013-10-17T08:31:37+04:00312013bEurope/MoscowThu, 17 Oct 2013 08:31:37 +0400 2013, 08:31:37
0
Решение

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)
}
}
ответил Dima 3 MonEurope/Moscow2018-12-03T19:12:27+03:00Europe/Moscow12bEurope/MoscowMon, 03 Dec 2018 19:12:27 +0300 2018, 19:12:27
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. Обновленный ответ Лапша из Death`s ответить . Суб>

ответил Hemang 12 MarpmMon, 12 Mar 2018 14:35:12 +03002018-03-12T14:35:12+03:0002 2018, 14:35:12
0

Правильный ответ: ты на самом деле не можешь. Большинство решений здесь пытаются воссоздать внутреннее поведение UILabel с переменной точностью. Так уж получилось, что в последних версиях iOS UILabel отображает текст с использованием каркасов TextKit /CoreText. В более ранних версиях он фактически использовал WebKit под капотом. Кто знает, что он будет использовать в будущем. Воспроизведение с помощью TextKit имеет явные проблемы даже сейчас (не говоря о перспективных решениях). Мы не совсем знаем (или не можем гарантировать), как на самом деле выполняется макетирование текста и рендеринг, то есть какие параметры используются - просто посмотрите на все упомянутые крайние случаи. Некоторое решение может «случайно» сработать для вас в некоторых простых случаях, которые вы тестировали - это ничего не гарантирует, даже в текущей версии iOS. Рендеринг текста сложен, не начинайте с поддержки RTL, Dynamic Type, Emojis и т. Д.

Если вам нужно правильное решение, просто не используйте UILabel. Это простой компонент, и тот факт, что его внутренние компоненты TextKit не представлены в API, является явным свидетельством того, что Apple не хочет, чтобы разработчики создавали решения, опираясь на эту деталь реализации.

В качестве альтернативы вы можете использовать UITextView. Он удобно раскрывает свои внутренние компоненты TextKit и легко настраивается, так что вы можете достичь почти всего, что UILabel хорошо для.

Вы также можете просто перейти на более низкий уровень и напрямую использовать TextKit (или даже ниже с CoreText). Скорее всего, если вам действительно нужно найти текстовые позиции, вам вскоре могут понадобиться другие мощные инструменты, доступные в этих рамках. Поначалу их довольно сложно использовать, но я чувствую, что большая часть сложности заключается в том, чтобы на самом деле узнать, как работает макетирование текста и рендеринг, - и это очень важный и полезный навык. Когда вы ознакомитесь с отличными текстовыми фреймворками Apple, вы почувствуете, что можете сделать гораздо больше с текстом.

ответил Max O 7 Jpm1000000pmMon, 07 Jan 2019 18:56:07 +030019 2019, 18:56:07
0

Другой способ сделать это, если у вас включена автоматическая настройка размера шрифта, будет выглядеть так:

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
ответил freshking 17 FebruaryEurope/MoscowbTue, 17 Feb 2015 13:16:50 +0300000000pmTue, 17 Feb 2015 13:16:50 +030015 2015, 13:16:50

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132