Für eine gegebene NSRange
würde ich gerne ein CGRect
in 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.
Der Trick ist, dass der UILabel
Text mehrere Zeilen hat und der Text wirklich ist. Daher attributedText
ist es etwas schwierig, die genaue Position der Zeichenfolge zu finden.
Die Methode, die ich in meine UILabel
Unterklasse 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
UITextView
undUITextField
anstatt aufUILabel
. - 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
NSLayoutManager
und 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
Antworten:
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]; }
quelle
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)
quelle
var glyphRange = NSRange() layoutManager.characterRangeForGlyphRange(range, actualGlyphRange: &glyphRange)
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);
Mein Vorschlag wäre, Text Kit zu verwenden. Leider haben wir keinen Zugriff auf den Layout-Manager, den ein
UILabel
verwendet. 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
NSTextStorage
Objekt zu erstellen , das genau den gleichen zugeordneten Text enthält, der in Ihrem Etikett enthalten ist. Erstellen Sie als Nächstes einNSLayoutManager
und fügen Sie es dem Textspeicherobjekt hinzu. Erstellen Sie abschließend eineNSTextContainer
mit 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 mitglyphRangeForCharacterRange: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
UILabel
selbst sollten gute Erfolgschancen bestehen.quelle
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) } }
quelle
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.
quelle
Die richtige Antwort ist, dass Sie es tatsächlich nicht können. Die meisten Lösungen hier versuchen,
UILabel
internes Verhalten mit unterschiedlicher Präzision wiederherzustellen . Es kommt einfach so vor, dass in neueren iOS-VersionenUILabel
rendert 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
UILabel
es 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.
quelle
Für alle, die eine
plain text
Erweiterung 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 .
quelle
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
quelle