Jak zlokalizować cgrect dla fragmentu tekstu w etykiecie UILabel?

Dla danego NSRange, chciałbym znaleźć {[2] } w UILabel, który odpowiada glifom tego NSRange. Na przykład chciałbym znaleźć CGRect, który zawiera słowo " pies "w zdaniu" szybki brązowy lis przeskakuje nad leniwym psem."

Wizualny opis problemu

Sztuczka polega na tym, że UILabel ma wiele linii, a tekst jest naprawdę attributedText, więc trudno jest znaleźć dokładną pozycję łańcucha.

Metoda, którą chciałbym napisać na mojej podklasie UILabel wyglądałoby to mniej więcej tak:

 - (CGRect)rectForSubstringWithRange:(NSRange)range;

szczegóły, dla zainteresowanych:

Moim celem jest to, aby móc stworzyć nowy UILabel z dokładnym wyglądem i pozycją UILabel, który następnie mogę animować. mam resztę rozpracowaną, ale to właśnie ten krok powstrzymuje mnie w tej chwili.

Co zrobiłem, aby spróbować rozwiązać problem do tej pory:

  • miałem nadzieję, że z iOS 7, byłoby trochę zestawu tekstowego, który rozwiązałby ten problem, ale większość przykładów, które widziałem z zestawem tekstowym, skupia się na UITextView i UITextField, a nie na UILabel.
  • widziałem inne pytanie na Stack Overflow tutaj, które obiecuje rozwiązać problem, ale przyjęta odpowiedź ma ponad dwa lata, a kod nie działa dobrze z przypisanym tekstem.

Założę się, że właściwa odpowiedź na to pytanie obejmuje jedną z następujących kwestii:

  • używanie standardowego tekstu Metoda Kit, aby rozwiązać ten problem w jednej linii kodu. Założę się, że obejmowałoby NSLayoutManager i textContainerForGlyphAtIndex:effectiveRange
  • pisanie złożonej metody, która rozbija UILabel na linie i znajduje rect glifu w linii, prawdopodobnie używając podstawowych metod tekstowych. Moim obecnym najlepszym rozwiązaniem jest rozebranie @mattt ' S excellent TTTAttributedLabel , który ma metodę, która znajduje glif w punkcie - jeśli go odwrócę i znajdę punkt dla glifu, może to praca.

Aktualizacja: oto GitHub gist z trzema rzeczami, które próbowałem do tej pory rozwiązać ten problem: https://gist.github.com/bryanjclark/7036101

Author: Community, 2013-10-17

8 answers

Po odpowiedź Joshuy w kodzie, wymyśliłem następujące, które wydają się działać dobrze:

- (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];
}
 92
Author: Luke Rogers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:54:39

Budowanie z Luke Rogers ' s answer ale napisane w 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)        
    }
}

Przykładowe Użycie (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)       
    }
}

Przykładowe Użycie (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)
 20
Author: NoodleOfDeath,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-11-28 14:50:58

Moją propozycją byłoby użycie zestawu tekstowego. Niestety nie mamy dostępu do menedżera układu, którego używa UILabel, jednak może być możliwe utworzenie jego repliki i użycie jej do uzyskania rect dla zakresu.

Moją sugestią byłoby utworzenie NSTextStorage obiektu zawierającego dokładnie ten sam przypisany tekst, jaki znajduje się w Twojej etykiecie. Następnie utwórz NSLayoutManager i dodaj ją do obiektu magazynowania tekstu. Na koniec utwórz NSTextContainer o tym samym rozmiarze co etykieta i dodaj ją do układu manager.

Teraz magazyn tekstu ma ten sam tekst, co etykieta, a kontener tekstowy ma ten sam rozmiar, co etykieta, więc powinniśmy być w stanie zapytać menedżera układu, który stworzyliśmy, o rect dla naszego zakresu za pomocą boundingRectForGlyphRange:inTextContainer:. Upewnij się, że najpierw przekonwertujesz zakres znaków na zakres glifów za pomocą glyphRangeForCharacterRange:actualCharacterRange: w obiekcie layout manager.

Wszystko idzie dobrze, co powinno dać ci CGRect zakresu określonego w etykiecie.

nie testowałem tego ale to byłoby moje podejście i naśladując działanie UILabel powinno mieć duże szanse na sukces.

 13
Author: Joshua,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-27 16:42:41

Swift 4 rozwiązanie, będzie działać nawet dla ciągów wielowierszowych, bounds zostały zastąpione przez 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)
}
}
 5
Author: Dima,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-12-03 16:12:27

Czy możesz zamiast tego oprzeć swoją klasę na UITextView? Jeśli tak, sprawdź metody protokołu UiTextInput. Zobacz w szczególności geometrię i metody odpoczynku hit.

 1
Author: pickwick,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-17 04:31:37

Właściwa odpowiedź jest taka, że rzeczywiście nie można. większość rozwiązań tutaj próbuje odtworzyć UILabel wewnętrzne zachowanie z różną precyzją. Tak się składa, że w ostatnich wersjach iOS UILabel renderuje tekst za pomocą frameworków TextKit/CoreText. We wcześniejszych wersjach faktycznie używał Webkita pod maską. Kto wie, co wykorzysta w przyszłości. Odtwarzanie za pomocą TextKit ma oczywiste problemy nawet teraz (nie mówiąc o przyszłościowych rozwiązaniach). Nie do końca wiemy (lub nie możemy zagwarantować) jak układ tekstu i renderowanie są faktycznie wykonywane, tzn. jakie parametry są używane – wystarczy spojrzeć na wszystkie wspomniane przypadki krawędzi. Niektóre rozwiązanie może "przypadkowo" działać dla niektórych prostych przypadkach, które przetestowałeś – to nie gwarantuje niczego nawet w bieżącej wersji iOS. Renderowanie tekstu jest trudne, nie każ mi zaczynać od obsługi RTL, dynamicznego typu, Emotikon itp.

Jeśli potrzebujesz odpowiedniego roztworu po prostu nie używaj UILabel. Jest to prosty komponent, a fakt, że jego TextKit wewnętrzne nie są ujawnione w API jest wyraźną wskazówką, że Apple nie chce, aby deweloperzy budowali rozwiązania opierając się na tych szczegółach implementacji.

Alternatywnie możesz użyć UITextView. Wygodnie eksponuje wewnętrzne elementy TextKit i jest bardzo konfigurowalny, dzięki czemu można osiągnąć prawie wszystko UILabel jest dobre dla.

Możesz również przejść na niższy poziom i używać TextKit bezpośrednio(lub nawet niżej z CoreText). Jest szansa, że jeśli naprawdę potrzebujesz znaleźć pozycje tekstowe, wkrótce możesz potrzebować innych potężnych narzędzia dostępne w tych ramach. Na początku są raczej trudne w użyciu, ale czuję, że większość złożoności pochodzi z uczenia się, jak działa układ tekstu i renderowanie – i to jest bardzo istotna i przydatna umiejętność. Po zapoznaniu się z doskonałymi strukturami tekstowymi firmy Apple poczujesz się upoważniony do zrobienia znacznie więcej z tekstem.

 1
Author: Max O,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-01-07 15:56:07

Dla każdego, kto szuka plain text rozszerzenia!

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. zaktualizowana ODPOWIEDŹ Noodle of Death ' s odpowiedź .

 0
Author: Hemang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-12 11:35:12

Inny sposób, jeśli masz włączoną automatyczną regulację rozmiaru czcionki, będzie taki:

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
Author: freshking,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-26 11:37:12