it-swarm.com.ru

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

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

Visual description of problem

Хитрость в том, что 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 Gist с тремя вещами, которые я до сих пор пытался решить эту проблему: https://Gist.github.com/bryanjclark/7036101

67
bryanjclark

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

- (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];
}
87
Luke Rogers

Построение из ответ Люка Роджерса но написано в 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)
16
NoodleOfDeath

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

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

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

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

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

11
Joshua

Решение 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)
}
}
2
Dima

Переведено для 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)
    }
2
Nico Achkasov

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

1
pickwick

Для тех, кто ищет расширение 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`sanswer .

0
Hemang

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

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

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

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

0
Max O