Як знайти CGRect для підрядка тексту в UILabel?


78

Для певного NSRangeя хотів би знайти a CGRectв, UILabelякий відповідає гліфам цього NSRange. Наприклад, я хотів би знайти те, CGRectщо містить слово "собака" у реченні "Швидка коричнева лисиця стрибає над ледачою собакою".

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

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

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

 - (CGRect)rectForSubstringWithRange:(NSRange)range;

Деталі для тих, хто цікавиться:

Моя мета - це можливість створити новий UILabel з точним виглядом і положенням UILabel, який я зможу потім анімувати. Решта я зрозумів, але саме цей крок зараз мене стримує.

Що я зробив, щоб вирішити проблему дотепер:

  • Я сподівався , що з прошивкою 7, НЕ було б трохи тексту Kit , який дозволить вирішити цю проблему, але майже кожен приклад , який я бачив з Text Kit фокусується на UITextViewі UITextField, а не UILabel.
  • Я бачив тут ще одне запитання щодо переповнення стека, яке обіцяє вирішити проблему, але прийнята відповідь старша за два роки, і код погано працює з атрибутивним текстом.

Б'юся об заклад, що правильна відповідь на це передбачає одне з наступного:

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

Оновлення: Ось суть github із трьома речами, які я намагався вирішити цю проблему: https://gist.github.com/bryanjclark/7036101


Чорт! Чому nslayoutmanager не виставляється на uilabel. Спроба вирішити одне і те ж. Ви придумали рішення?
Боб Спрін,

Ще не повернувся до цього питання, але після приблизно півдюжини різних спроб на цей момент, я найкраще сподіваюся здійснити щось подібне до рекомендації Джошуа нижче. Поділюсь тим, що з’ясую, коли зроблю!
bryanjclark

1
Можливо, не на 100% те, що вам потрібно, але перевірте github.com/SebastienThiebaud/STTweetLabel . Він використовує перегляд тексту для зберігання та обчислення. Додавати те, що вам потрібно, потрібно досить просто як посилання.
Боб Спрін,

Дякую, Боб! Одного разу я розглядав можливість використання STTweetLabel, але замість цього завершив створення власної варіації на TTTAttributedLabel - я розгляну STTweetLabel і побачу, як вони вирішили проблему. Здається, це на правильному шляху.
bryanjclark

1
Код Люка працює майже весь час. Але іноді повернутий прямокутник дорівнює нулю. Через довгий час для вирішення неправильного результату я виявив, що краще використовувати UITextView, використовувати textContainer та layoutManager UITextView, результат є ціннішим. Я думаю, можливо, є якась різниця між внутрішнім текстовим контейнером UILabel та тим, який генерується кодом Люка.
Ден Лі

Відповіді:


92

Після відповіді Джошуа в коді я придумав наступне, що, здається, працює добре:

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

9
Дуже круто! Однак людям слід пам’ятати, що це працює неправильно, якщо шрифт автоматично зменшився відповідно до поточного розміру подання. Вам потрібно відповідно відрегулювати розмір шрифту.
Арсеній

Цей код чудово працює на iOS7. Хто-небудь пробував це на iOS6, у моєму коді він отримує збій на iOS6 з журналом [__NSCFType _isDefaultFace]: невпізнаний селектор надіслано до екземпляра. Хтось має про це уявлення?
Richa,

3
Чудова відповідь. Оскільки UILabels не мають лівих полів, переконайтеся, що ви додали textContainer.lineFragmentPadding = 0;
colinbrash

Я використовую це, і це чудово працює, але працює не для кожного пройденого діапазону. Дивіться мою публікацію SO тут, де описано більш детально.
Стівен

2
Дивовижно, однак рішення не враховує вирівнювання для attributedText.
Габріель Массана

20

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

Стрімкий 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)

Стрім 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)

1
Я вирішив аварію цим:var glyphRange = NSRange() layoutManager.characterRangeForGlyphRange(range, actualGlyphRange: &glyphRange)
Соня Касас

1
@Nemanja, як ти розбиваєш діапазони між рядками? Я хочу, щоб це було так: imgur.com/a/P9RzR, але коли текст ближче до кінця, це не працює.
demiculus

4
Важливо зазначити, що атрибутивний текст вашого UILabel повинен мати атрибути для ключів NSAttributedStringKey.paragraphStyle та NSAttributedStringKey.font, щоб ця функція могла враховувати це вирівнювання тексту та шрифт
Zigglzworth

3
Приклад використання @Hemang:let t = "aa bb cc"; label.attributedText = NSAttributedString(string: t); let l = CALayer(); l.borderWidth = 1; l.frame = label.boundingRectForCharacterRange(NSRange(t.range(of: "bb")!, in: t)); label.layer.addSublayer(l);
Cœur

1
Якщо ви бачите, що він не працює для центруваного тексту, не використовуйте вирівнювання мітки, відцентруйте атрибутивний рядок ...
Ixx

13

Моя пропозиція полягала б у використанні Text Kit. На жаль, ми не маємо доступу до менеджера макета, який aUILabel використовує однак, можливо, можна створити його копію та використовувати його, щоб отримати прямокутник для діапазону.

Моя пропозиція полягала б у створенні NSTextStorageоб’єкта, що містить точно той самий атрибутивний текст, що й у вашій мітці. Далі створіть NSLayoutManagerі додайте його до об’єкта зберігання тексту. Нарешті створіть файл NSTextContainerтакого ж розміру, що і мітка, і додайте його до менеджера макета.

Тепер сховище тексту має той самий текст, що і мітка, а текстовий контейнер має той самий розмір, що і мітка, тому ми повинні мати можливість попросити менеджер макета, який ми створили, про рект для нашого діапазону boundingRectForGlyphRange:inTextContainer:. Переконайтеся, що спочатку перетворили діапазон символів на діапазон гліфів, використовуючи glyphRangeForCharacterRange:actualCharacterRange:об’єкт менеджера макета.

Все добре, що повинно дати вам обмеження CGRect діапазону, який ви вказали в етикетці.

Я не тестував цього, але це був би мій підхід, і імітуючи те, як UILabelпрацює саме, має мати великі шанси на успіх.


Це звучить як хороша ставка - я спробую зробити це та повідомити про це пізніше! Якщо це вдасться, я надам код, який закінчився працювати для мене на GitHub. Дякую!
bryanjclark

@bryanjclark Вам пощастило з цим?
Джошуа

Я зробив саме це, і це працює, але не ідеально, у мене трохи компенсується, тому він не може правильно розпізнати торкнутеся слово, і він працює, коли торкається того, що знаходиться збоку ...
Андрес

5

швидке рішення 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)
}
}

на жаль, це просто не працює: / він просто надає intrinsicContentSize ("загальний типографський розмір", а не фактичну форму гліфа). Ви можете побачити результат тут: stackoverflow.com/questions/56935898
Fattie

1

Чи можете ви замість цього покласти свій клас на UITextView? Якщо так, перевірте методи протоколу UiTextInput. Дивіться, зокрема, геометрію та методи відпочинку.


На жаль, я не думаю, що це був би прагматичний вибір тут. Мій UILabel - це спеціальний перезапис TTTAttributedLabel, і я б не хотів переписувати величезну кількість коду, щоб створити цей єдиний метод, якщо це можливо. Хоча дякую!
bryanjclark

@bryanjclark ти коли-небудь знаходив рішення?
Jenel Ejercito Myers

1

Правильна відповідь - ви насправді не можете. Більшість рішень тут намагаються відтворитиUILabel внутрішню поведінку з різною точністю. Так трапляється, що в останніх версіях iOSUILabelвідображає текст за допомогою фреймворків TextKit / CoreText. У попередніх версіях він фактично використовував WebKit під капотом. Хто знає, чим він буде користуватися в майбутньому. Відтворення за допомогою TextKit має очевидні проблеми навіть зараз (не кажучи вже про рішення, що захищають майбутнє). Ми не зовсім знаємо (або не можемо гарантувати), як насправді виконуються макет тексту та візуалізація, тобто які параметри використовуються - просто подивіться на всі згадані випадки ребер. Деякі рішення можуть "випадково" працювати для вас у деяких простих випадках, які ви перевірили - це не гарантує нічого навіть у поточній версії iOS. Візуалізація тексту важка, не починайте мене на підтримці RTL, Dynamic Type, смайликах тощо.

Якщо вам потрібне належне рішення, просто не використовуйте UILabel. Це простий компонент, і той факт, що його внутрішні елементи TextKit не виставляються в API, є чітким свідченням того, що Apple не хоче, щоб розробники створювали рішення, спираючись на цю деталь реалізації.

Ви також можете використовувати UITextView. Він зручно виставляє внутрішні елементи TextKit і дуже налаштовується, тому ви можете досягти майже всього UILabel, що добре.

Ви також можете просто перейти на нижчий рівень і використовувати TextKit безпосередньо (або навіть нижче з CoreText). Швидше за все, якщо вам дійсно потрібно знайти текстові позиції, незабаром вам можуть знадобитися інші потужні інструменти, доступні в цих рамках. Спочатку їх досить важко використовувати, але я відчуваю, що більша частина складності пов’язана з тим, що насправді вивчають, як працює розмітка та рендеринг тексту - і це дуже доречна та корисна навичка. Ознайомившись із чудовими текстовими рамками Apple, ви відчуєте можливість робити набагато більше з текстом.


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)
    }
}

PS Оновлений відповідь локшини з Death`s відповіді .


.attributedText сумісний з .text: ви отримаєте той самий CGRect.
Cœur

на жаль, це просто не працює. Ви можете побачити результати тут: stackoverflow.com/questions/56935898
Fattie

-1

Інший спосіб зробити це, якщо у вас увімкнено автоматичне регулювання розміру шрифту, буде таким:

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
Що таке "CalculationHelper"?
Carmelo Gallo

Це просто клас, який я написав для обчислення рядків rect
freshking

2
Так, я знаю, що це клас :) Не могли б ви поділитися ним? В іншому випадку ваш код неповний;)
Carmelo Gallo

2
Я не мав на увазі boundingRectWithSize: але якби ви могли поділитися класом CalculationHelper. У будь-якому разі, я це сам зрозумів;)
Carmelo Gallo

Посилання @freshking на boundingRectWithSize тепер має бути developer.apple.com/documentation/foundation/nsstring/… . Я включив її безпосередньо до Вашої відповіді, щоб уникнути неповного коду.
Cœur,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.