ios ios7 uilabel textkit

ios - ¿Cómo ubico el CGRect para una subcadena de texto en un UILabel?



ios7 textkit (7)

¡Para cualquiera que esté buscando una extensión de 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 Respuesta actualizada de la respuesta de Noodle of Death .

Para un NSRange dado, me gustaría encontrar un CGRect en un UILabel que corresponda a los glifos de ese NSRange . Por ejemplo, me gustaría encontrar el CGRect que contiene la palabra "perro" en la oración "El zorro marrón rápido salta sobre el perro flojo".

El truco es que el UILabel tiene múltiples líneas, y el texto es realmente texto attributedText , por lo que es un poco difícil encontrar la posición exacta de la cuerda.

El método que me gustaría escribir en mi subclase UILabel sería algo como esto:

- (CGRect)rectForSubstringWithRange:(NSRange)range;

Detalles, para quienes estén interesados:

Mi objetivo con esto es poder crear una nueva UILabel con la apariencia y posición exacta de UILabel, que luego puedo animar. Tengo el resto resuelto, pero es este paso en particular el que me está frenando en este momento.

Lo que hice para tratar de resolver el problema hasta ahora:

  • Esperaba que con iOS 7, hubiera un poco de Text Kit que resolvería este problema, pero la mayoría de los ejemplos que he visto con Text Kit se centran en UITextView y UITextField , en lugar de UILabel .
  • He visto otra pregunta sobre Desbordamiento de pila aquí que promete solucionar el problema, pero la respuesta aceptada tiene más de dos años y el código no funciona bien con el texto atribuido.

Apuesto a que la respuesta correcta a esto involucra uno de los siguientes:

  • Usar un método estándar de Kit de texto para resolver este problema en una sola línea de código. Apuesto a que implicaría NSLayoutManager y textContainerForGlyphAtIndex:effectiveRange
  • Escribir un método complejo que divide el UILabel en líneas y encuentra el rect de un glifo dentro de una línea, probablemente usando métodos de Texto central. Mi mejor apuesta actual es @mattt''s excelente TTTAttributedLabel @mattt''s , que tiene un método que encuentra un glifo en un punto; si lo invierto y encuentre el punto para un glifo, podría funcionar.

Actualización: Aquí hay un github gist con las tres cosas que he intentado hasta ahora para resolver este problema: https://gist.github.com/bryanjclark/7036101


¿Puedes basar tu clase en UITextView? Si es así, consulte los métodos de protocolo UiTextInput. Consulte en particular la geometría y los métodos de descanso de golpe.


Basándose en la respuesta de Luke Rogers, pero escrita en breve:

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

Ejemplo de uso (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

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

Ejemplo de uso (Swift 3)

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)


Mi sugerencia sería hacer uso de Text Kit. Lamentablemente, no tenemos acceso al administrador de diseño que utiliza UILabel sin embargo, es posible crear una réplica del mismo y usarlo para obtener el rect de un rango.

Mi sugerencia sería crear un objeto NSTextStorage que contenga exactamente el mismo texto atribuido que figura en su etiqueta. A continuación, cree un NSLayoutManager y agréguelo al objeto de almacenamiento de texto. Finalmente, cree un NSTextContainer con el mismo tamaño que la etiqueta y agréguelo al administrador de diseño.

Ahora el almacenamiento de texto tiene el mismo texto que la etiqueta y el contenedor de texto tiene el mismo tamaño que la etiqueta, por lo que deberíamos poder solicitar el administrador de diseño que creamos para un rect para nuestro rango usando boundingRectForGlyphRange:inTextContainer: Asegúrese de convertir el rango de caracteres en un rango de glifos primero usando glyphRangeForCharacterRange:actualCharacterRange: en el objeto del administrador de glyphRangeForCharacterRange:actualCharacterRange: .

Todo va bien y debería proporcionarle un CGRect límite del rango que especificó en la etiqueta.

No he probado esto, pero este sería mi enfoque y al imitar cómo funciona el UILabel sí debería tener una buena posibilidad de éxito.


Otra forma de hacerlo, si tiene habilitado el ajuste automático del tamaño de fuente, sería así:

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


Siguiendo la respuesta de Joshua en código, se me ocurrió lo siguiente que parece funcionar bien:

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


Traducido para Swift 3.

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