как найти cgrect для подстроки текста в uilabel?

<p>для данного <code>nsrange</code> я хотел бы найти <code>cgrect</code> в <code>uilabel</code>, который соответствует глифам этого <code>nsrange</code>. например, я хотел бы найти <code>cgrect</code>, который содержит слово «собака» в предложении «быстрая коричневая лиса перепрыгивает через ленивую собаку».</p><p><img src="https://i.stack.imgur.com/khgff.png" alt="visual description of problem"></p><p>хитрость в том, что <code>uilabel</code> имеет несколько строк, а текст действительно <code>attributedtext</code>, поэтому довольно сложно найти точное положение строки.</p><p>метод, который я хотел бы написать в моем подклассе <code>uilabel</code>, выглядел бы примерно так:</p><pre><code> - (cgrect)rectforsubstringwithrange:(nsrange)range; </code></pre><hr><p><em>подробности, для тех, кто заинтересован:</em></p><p>моя цель в этом заключается в том, чтобы иметь возможность <em>создать новую uilabel с точным внешним видом и положением uilabel, которую я затем смогу анимировать.</em> у меня есть все остальное, но это, в частности, этот шаг это сдерживает меня в данный момент.</p><p><strong>что я сделал, чтобы попытаться решить проблему до сих пор:</strong></p><ul> <li>я надеялся, что в ios 7 найдется немного text kit, который решит эту проблему, но почти каждый пример, который я видел с text kit, фокусируется на <code>uitextview</code> и <code>uitextfield</code>, а не <code>uilabel</code>.</li> <li>я видел еще один вопрос о переполнении стека, который обещает решить проблему, но принятый ответ уже более двух лет, и код не работает с атрибутивным текстом.</li> </ul><p><strong>могу поспорить, что правильный ответ на этот вопрос предполагает одно из следующих действий:</strong></p><ul> <li>использование стандартного метода text kit для решения этой проблемы в одной строке кода. могу поспорить, что это будет связано с <code>nslayoutmanager</code> и <code>textcontainerforglyphatindex:effectiverange</code></li> <li>написание сложного метода, который разбивает uilabel на строки и находит прямоугольник глифа в строке, вероятно, с использованием методов core text. моя текущая лучшая ставка - разобрать <a href="https://stackoverflow.com/users/157142/mattt">@ mattt's</a> превосходный <a href="https://github.com/mattt/tttattributedlabel">tttattributedlabel</a> , у которого есть метод, который находит глиф в точке - если я инвертирую это, и найду точку для глифа, которая может работать.</li> </ul><hr><p>обновление: вот github gist с тремя вещами, которые я до сих пор пытался решить эту проблему: <a href="https://gist.github.com/bryanjclark/7036101">https://gist.github.com/bryanjclark/7036101</a> </p>

<p>следуя <a href="https://stackoverflow.com/a/19620560/1129789">ответу джошуа</a> в коде, я пришел к следующему, которое, кажется, работает хорошо:</p><pre><code>- (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]; } </code></pre>

<p>построение из <a href="https://stackoverflow.com/a/20633388/409958">ответ люка роджерса</a> но написано в swift:</p><p><strong>swift 2</strong></p><pre><code>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) } } </code></pre><p><strong>пример использования (swift 2)</strong></p><pre><code>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) </code></pre><p><strong>swift 3/4</strong></p><pre><code>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) } } </code></pre><p><strong>пример использования (swift 3/4)</strong></p><pre><code>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) </code></pre>

<p>мое предложение будет использовать text kit. к сожалению, у нас нет доступа к диспетчеру компоновки, который использует <code>uilabel</code>, однако возможно создать его копию и использовать ее для получения прямоугольника для диапазона. </p><p>я бы предложил создать объект <code>nstextstorage</code>, содержащий точно такой же атрибутный текст, как и в вашем ярлыке. затем создайте <code>nslayoutmanager</code> и добавьте его к объекту хранения текста. наконец, создайте <code>nstextcontainer</code> того же размера, что и метка, и добавьте его в менеджер макета.</p><p>теперь текстовое хранилище имеет тот же текст, что и метка, а текстовый контейнер имеет тот же размер, что и метка, поэтому мы должны иметь возможность попросить менеджера компоновки, который мы создали, для прямоугольника для нашего диапазона, используя <code>boundingrectforglyphrange:intextcontainer:</code>. убедитесь, что сначала вы преобразуете свой диапазон символов в диапазон глифов, используя <code>glyphrangeforcharacterrange:actualcharacterrange:</code> на объекте менеджера компоновки. </p><p>все идет хорошо, что должно дать вам<strong> ограничивающий </strong><code>cgrect</code> диапазона, который вы указали в метке. </p><p><em>я не проверял это, но это был бы мой подход, и, имитируя, как работает само <code>uilabel</code>, вы должны иметь хорошие шансы на успех.</em></p>

<p>решение swift 4, будет работать даже для многострочных строк, границы были заменены на intrinsiccontentsize</p><pre><code>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) } } </code></pre>

<p><strong> переведено для swift 3. </strong></p><pre><code>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) } </code></pre>

<p>можете ли вы вместо этого основывать свой класс на uitextview? если так, проверьте методы протокола uitextinput. посмотрите, в частности, геометрию и методы покоя. </p>

<p>для тех, кто ищет расширение <code>plain text</code>!</p><pre><code>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) } } </code></pre><p><sub>постскриптум обновленный ответ <strong>noodle of death`s</strong> <a href="https://stackoverflow.com/a/37690015/1603234">answer</a> .</sub></p>

<p>правильный ответ: ты на самом деле не можешь. большинство решений здесь пытаются воссоздать <code>uilabel</code> внутреннее поведение с переменной точностью. просто так получилось, что в последних версиях ios <code>uilabel</code> отображает текст, используя платформы textkit/coretext. в более ранних версиях он фактически использовал webkit под капотом. кто знает, что он будет использовать в будущем. воспроизведение с помощью textkit имеет явные проблемы даже сейчас (не говоря о перспективных решениях). мы не совсем знаем (или не можем гарантировать), как на самом деле выполняется макетирование текста и рендеринг, то есть какие параметры используются - просто посмотрите на все упомянутые случаи edge. некоторое решение может «случайно» сработать для вас в некоторых простых случаях, которые вы тестировали, - это ничего не гарантирует даже в рамках текущей версии ios. рендеринг текста сложен, не начинайте с поддержки rtl, dynamic type, emojis и т.д.</p><p>если вам нужно правильное решение, просто не используйте <code>uilabel</code>. это простой компонент, и тот факт, что его внутренние компоненты textkit не представлены в api, является явным свидетельством того, что apple не хочет, чтобы разработчики создавали решения, опираясь на эту деталь реализации.</p><p>в качестве альтернативы вы можете использовать uitextview. он удобно раскрывает свои внутренние компоненты textkit и легко настраивается, так что вы можете достичь почти всего, для чего <code>uilabel</code> хорош.</p><p>вы также можете просто перейти на более низкий уровень и напрямую использовать textkit (или даже понизить его с coretext). скорее всего, если вам действительно нужно найти текстовые позиции, вам вскоре могут понадобиться другие мощные инструменты, доступные в этих рамках. поначалу их довольно сложно использовать, но я чувствую, что большая часть сложности заключается в том, чтобы на самом деле узнать, как работает верстка текста и рендеринг, - и это очень актуальный и полезный навык. когда вы ознакомитесь с отличными текстовыми фреймворками apple, вы почувствуете, что можете сделать гораздо больше с текстом.</p>