Подтвердить что ты не робот

Как найти 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

4b9b3361

Ответ 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