Wie finde ich das CGRect für eine Teilzeichenfolge von Text in einem UILabel?


78

Für eine gegebene NSRangewürde ich gerne ein CGRectin einem finden UILabel, das den Glyphen davon entspricht NSRange. Zum Beispiel möchte ich das finden CGRect, das das Wort "Hund" im Satz "Der schnelle braune Fuchs springt über den faulen Hund" enthält.

Visuelle Beschreibung des Problems

Der Trick ist, dass der UILabelText mehrere Zeilen hat und der Text wirklich ist. Daher attributedTextist es etwas schwierig, die genaue Position der Zeichenfolge zu finden.

Die Methode, die ich in meine UILabelUnterklasse schreiben möchte, würde ungefähr so ​​aussehen:

 - (CGRect)rectForSubstringWithRange:(NSRange)range;

Details für Interessierte:

Mein Ziel dabei ist es , ein neues UILabel mit dem genauen Erscheinungsbild und der Position des UILabels erstellen zu können, das ich dann animieren kann. Ich habe den Rest herausgefunden, aber gerade dieser Schritt hält mich im Moment zurück.

Was ich bisher getan habe, um das Problem zu lösen:

  • Ich hatte gehofft, dass es mit iOS 7 ein bisschen Text Kit geben würde, das dieses Problem lösen würde, aber fast jedes Beispiel, das ich mit Text Kit gesehen habe, konzentriert sich auf UITextViewund UITextFieldanstatt auf UILabel.
  • Ich habe hier eine andere Frage zu Stack Overflow gesehen, die verspricht, das Problem zu lösen, aber die akzeptierte Antwort ist über zwei Jahre alt und der Code funktioniert mit zugeordnetem Text nicht gut.

Ich wette, dass die richtige Antwort darauf eine der folgenden beinhaltet:

  • Verwenden einer Standard-Text-Kit-Methode, um dieses Problem in einer einzigen Codezeile zu lösen. Ich wette, es würde NSLayoutManagerund beinhaltentextContainerForGlyphAtIndex:effectiveRange
  • Schreiben einer komplexen Methode, die das UILabel in Zeilen aufteilt und das Rechteck eines Glyphen innerhalb einer Zeile findet, wahrscheinlich unter Verwendung von Core-Text-Methoden. Meine derzeit beste Wette ist es, @ mattts exzellentes TTTAttributedLabel zu zerlegen , das eine Methode hat, die eine Glyphe an einem Punkt findet - wenn ich das invertiere und den Punkt für eine Glyphe finde, könnte das funktionieren.

Update: Hier ist ein Github-Kern mit den drei Dingen, die ich bisher versucht habe, um dieses Problem zu lösen: https://gist.github.com/bryanjclark/7036101


Teufel noch mal. Warum ist nslayoutmanager nicht auf uilabel verfügbar? Der Versuch, das Gleiche zu lösen. Haben Sie eine Lösung gefunden?
Bob Spryn

Ich bin noch nicht auf dieses Problem zurückgekommen, aber nach ungefähr einem halben Dutzend verschiedener Versuche zu diesem Zeitpunkt besteht meine beste Hoffnung darin, etwas Ähnliches wie Joshuas Empfehlung unten umzusetzen. Ich werde teilen, was ich herausfinde, wenn ich es tue!
Bryanjclark

1
Vielleicht nicht 100% was Sie brauchen, aber besuchen Sie github.com/SebastienThiebaud/STTweetLabel . Er nutzt eine Textansicht zur Speicherung und Berechnung. Sollte ziemlich einfach sein, um das, was Sie brauchen, als Referenz hinzuzufügen.
Bob Spryn

Danke, Bob! Ich hatte schon einmal überlegt, STTweetLabel zu verwenden, aber stattdessen meine eigene benutzerdefinierte Variante von TTTAttributedLabel erstellt. Ich werde in STTweetLabel nachsehen, wie sie das Problem gelöst haben. Klingt so, als wäre das auf dem richtigen Weg.
Bryanjclark

1
Lukes Code funktioniert fast immer. Aber manchmal ist das zurückgegebene Rechteck Null. Nach langer Zeit zum Lösen des falschen Ergebnisses fand ich es besser, UITextView, den TextContainer und den layoutManager von UITextView zu verwenden. Das Ergebnis ist wertvoller. Ich denke, vielleicht gibt es einen Unterschied zwischen UILabels internem textContainer und dem, der durch Lukes Code generiert wurde.
Dan Lee

Antworten:


92

Nach Joshuas Antwort im Code habe ich Folgendes gefunden, das gut zu funktionieren scheint:

- (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
Sehr cool! Es sollte jedoch beachtet werden, dass dies nicht ordnungsgemäß funktioniert, wenn die Schriftart automatisch auf die aktuelle Ansichtsgröße verkleinert wurde. Sie müssen Ihre Schriftgröße entsprechend anpassen.
Arsenius

Dieser Code funktioniert einwandfrei unter iOS7. Hat jemand es unter iOS6 versucht, in meinem Code stürzt es unter iOS6 mit dem Protokoll [__NSCFType _isDefaultFace] ab: Nicht erkannter Selektor an Instanz gesendet. Hat jemand eine Idee dazu?
Richa

3
Gute Antwort. Da UILabels keine linken Ränder haben, stellen Sie sicher, dass Sie textContainer.lineFragmentPadding = 0 hinzufügen.
Colinbrash

Ich benutze dies und es funktioniert großartig, aber nicht für jeden Bereich, den ich passiere. Siehe meinen SO-Beitrag hier, der ausführlicher beschreibt.
Stephen

2
Genial, aber die Lösung berücksichtigt nicht die Ausrichtung für den zugeschriebenen Text.
Gabriel.Massana

20

Aufbauend auf Luke Rogers 'Antwort, aber schnell geschrieben:

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

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

Beispielverwendung (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
Ich habe den Absturz damit gelöst:var glyphRange = NSRange() layoutManager.characterRangeForGlyphRange(range, actualGlyphRange: &glyphRange)
Sonia Casas

1
@Nemanja wie unterbricht man die Bereiche zwischen den Zeilen? Ich möchte, dass es so ist wie imgur.com/a/P9RzR, aber wenn der Text sich dem Ende nähert , funktioniert es nicht.
Demiculus

4
Es ist wichtig zu beachten, dass der von Ihrem UILabel zugewiesene Text Attribute für die Schlüssel NSAttributedStringKey.paragraphStyle und NSAttributedStringKey.font enthalten muss, damit diese Funktion diese Textausrichtung und Schriftart berücksichtigen kann
Zigglzworth

3
@ Hämang Beispiel Verwendung: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
Wenn

13

Mein Vorschlag wäre, Text Kit zu verwenden. Leider haben wir keinen Zugriff auf den Layout-Manager, den ein UILabelverwendet. Es ist jedoch möglicherweise möglich, eine Replik davon zu erstellen und diese zu verwenden, um die Korrektur für einen Bereich zu erhalten.

Mein Vorschlag wäre, ein NSTextStorageObjekt zu erstellen , das genau den gleichen zugeordneten Text enthält, der in Ihrem Etikett enthalten ist. Erstellen Sie als Nächstes ein NSLayoutManagerund fügen Sie es dem Textspeicherobjekt hinzu. Erstellen Sie abschließend eine NSTextContainermit der gleichen Größe wie das Etikett und fügen Sie diese dem Layout-Manager hinzu.

Jetzt hat der Textspeicher den gleichen Text wie das Etikett und der Textcontainer hat die gleiche Größe wie das Etikett, sodass wir den von uns erstellten Layout-Manager nach einem Rect für unseren Bereich fragen können boundingRectForGlyphRange:inTextContainer:. Stellen Sie sicher, dass Sie Ihren Zeichenbereich zuerst mit glyphRangeForCharacterRange:actualCharacterRange:dem Layout-Manager-Objekt in einen Glyphenbereich konvertieren .

Alles läuft gut, das sollte Ihnen eine Begrenzung CGRect des Bereichs geben, den Sie auf dem Etikett angegeben haben.

Ich habe dies nicht getestet, aber dies wäre mein Ansatz und durch Nachahmung der Funktionsweise UILabelselbst sollten gute Erfolgschancen bestehen.


Das klingt nach einer guten Wette - ich werde es versuchen und später berichten! Wenn es klappt, werde ich den Code bereitstellen, der für mich auf GitHub funktioniert hat. Vielen Dank!
Bryanjclark

@bryanjclark Hattest du Glück damit?
Joshua

Ich habe genau das getan, und es funktioniert, aber nicht perfekt. Ich habe einen kleinen Versatz, sodass das berührte Wort nicht richtig erkannt wird, und es funktioniert, wenn das Wort berührt wird, das sich direkt auf der Seite befindet ...
Andres

5

Swift 4-Lösung, funktioniert auch für mehrzeilige Zeichenfolgen, Grenzen wurden durch intrinsicContentSize ersetzt

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

Leider funktioniert dies einfach nicht: / Es gibt nur die intrinsicContentSize an (die "typografische Gesamtgröße", nicht die tatsächliche Glyphenform). Sie können das Ergebnis hier sehen: stackoverflow.com/questions/56935898
Fattie

1

Können Sie Ihre Klasse stattdessen auf UITextView basieren? Wenn ja, überprüfen Sie die UiTextInput-Protokollmethoden. Siehe insbesondere die Geometrie und die Ruhemethoden für Treffer.


Leider denke ich nicht, dass dies hier die pragmatische Wahl wäre. Mein UILabel ist ein benutzerdefiniertes Umschreiben von TTTAttributedLabel, und ich würde es hassen, wenn ich eine große Menge Code umschreiben müsste, um diese eine Methode zu erstellen, wenn möglich. Trotzdem danke!
Bryanjclark

@bryanjclark hast du jemals eine Lösung gefunden?
Jenel Ejercito Myers

1

Die richtige Antwort ist, dass Sie es tatsächlich nicht können. Die meisten Lösungen hier versuchen, UILabelinternes Verhalten mit unterschiedlicher Präzision wiederherzustellen . Es kommt einfach so vor, dass in neueren iOS-VersionenUILabelrendert Text mit TextKit / CoreText-Frameworks. In früheren Versionen wurde WebKit tatsächlich unter der Haube verwendet. Wer weiß, was es in Zukunft verwenden wird. Das Reproduzieren mit TextKit hat bereits jetzt offensichtliche Probleme (ohne über zukunftssichere Lösungen zu sprechen). Wir wissen nicht genau (oder können nicht garantieren), wie das Textlayout und das Rendern tatsächlich ausgeführt werden, dh welche Parameter verwendet werden - sehen Sie sich nur alle genannten Randfälle an. Einige Lösungen funktionieren möglicherweise versehentlich für Sie in einigen einfachen Fällen, die Sie getestet haben - das garantiert auch in der aktuellen Version von iOS nichts. Das Rendern von Text ist schwierig. Lassen Sie mich nicht mit RTL-Unterstützung, dynamischem Typ, Emojis usw. beginnen.

Wenn Sie eine richtige Lösung benötigen, verwenden Sie diese einfach nicht UILabel. Es ist eine einfache Komponente, und die Tatsache, dass die TextKit-Interna nicht in der API verfügbar sind, ist ein klarer Hinweis darauf, dass Apple nicht möchte, dass Entwickler Lösungen entwickeln, die auf diesen Implementierungsdetails basieren.

Alternativ können Sie UITextView verwenden. Es macht die TextKit-Interna bequem sichtbar und ist sehr konfigurierbar, sodass Sie fast alles erreichen können, wofür UILabeles gut ist.

Sie können auch einfach auf eine niedrigere Ebene gehen und TextKit direkt verwenden (oder sogar mit CoreText niedriger). Wenn Sie tatsächlich Textpositionen suchen müssen, benötigen Sie möglicherweise bald andere leistungsstarke Tools, die in diesen Frameworks verfügbar sind. Sie sind anfangs ziemlich schwer zu verwenden, aber ich glaube, dass der größte Teil der Komplexität darin besteht, zu lernen, wie Textlayout und Rendering funktionieren - und das ist eine sehr relevante und nützliche Fähigkeit. Sobald Sie sich mit den hervorragenden Text-Frameworks von Apple vertraut gemacht haben, werden Sie sich befähigt fühlen, noch viel mehr mit Text zu tun.


0

Für alle, die eine plain textErweiterung suchen !

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 Antwort aktualisiert Noodle von Death`s Antwort .


.attributedText ist kompatibel mit .text: Sie würden das gleiche CGRect erhalten.
Cœur

Leider funktioniert das einfach nicht. Sie können die Ausgabe hier sehen: stackoverflow.com/questions/56935898
Fattie

-1

Wenn Sie die automatische Anpassung der Schriftgröße aktiviert haben, können Sie dies folgendermaßen tun:

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
Was ist "CalculationHelper"?
Carmelo Gallo

Das ist nur eine Klasse, die ich geschrieben habe, um die Saiten
richtig

2
Ja, ich weiß, dass es eine Klasse ist :) Könnten Sie sie bitte teilen? Andernfalls ist Ihr Code unvollständig;)
Carmelo Gallo

2
Ich meinte nicht die BoundingRectWithSize: aber wenn Sie die CalculationHelper-Klasse teilen könnten. Wie auch immer, ich habe es selbst herausgefunden;)
Carmelo Gallo

@freshking Link zu boundingRectWithSize sollte jetzt developer.apple.com/documentation/foundation/nsstring/… sein . Ich habe Ihre Antwort direkt eingefügt, um unvollständigen Code zu vermeiden.
Cœur
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.