nshtmltextdocumenttype attributed ios uilabel core-text typography nsattributedstring

ios - attributed - swift 4 uilabel html



CaĆ­da de la tapa con NSAttributedString (5)

Me gustaría hacer un primer carácter de UILabel en un UILabel utilizando la propiedad NSAttributedString solamente. Me gusta esto:

http://www.interpretationbydesign.com/wp-content/uploads/2009/02/drop_caps.gif

He experimentado con el ajuste de la línea de base para el rango del primer carácter a un valor negativo, y funciona para alinear la parte superior de la primera char con la parte superior del resto de la primera línea. Pero no he encontrado ninguna manera de hacer que las otras líneas fluyan a la derecha del carácter con límite de caída.

¿Se puede resolver esto usando NSAttributedString only , o tengo que dividir la cadena y renderizarla usando Core Text?


Como todos los demás han mencionado, no es posible hacer esto solo con NSAttributedString . Nikolai tiene el enfoque correcto, utilizando CTFrameSetters . Sin embargo, es posible decirle al creador de cuadros que represente el texto en un área específica (es decir, definido por un CGPath).

Tendrá que crear 2 marcos, uno para el límite inferior y el otro para el resto del texto.

Luego, toma el marco de la tapa de caída y crea un CGPathRef que se ejecuta alrededor del espacio del marco de la tapa de caída.

Luego, renderiza ambos marcos de cuadros en tu vista.

He creado un proyecto de ejemplo con un objeto llamado DropCapView que es una subclase de UIView. Esta vista representa el primer carácter y envuelve el texto restante a su alrededor.

Se parece a esto:

Hay unos cuantos pasos, así que he agregado un enlace a un proyecto de github que alberga el ejemplo. Hay comentarios en el proyecto que te ayudarán.

Proyecto DropCap en GitHub

Tendrá que jugar un poco con la forma del elemento textBox (es decir, el CGPathRef) para rellenar los bordes de la vista, y para ajustarla también a la letra mayúscula.

Aquí están las entrañas del método de dibujo:

- (void)drawRect:(CGRect)rect { //make sure that all the variables exist and are non-nil NSAssert(_text != nil, @"text is nil"); NSAssert(_textColor != nil, @"textColor is nil"); NSAssert(_fontName != nil, @"fontName is nil"); NSAssert(_dropCapFontSize > 0, @"dropCapFontSize is <= 0"); NSAssert(_textFontSize > 0, @"textFontSize is <=0"); //convert the text aligment from NSTextAligment to CTTextAlignment CTTextAlignment ctTextAlignment = NSTextAlignmentToCTTextAlignment(_textAlignment); //create a paragraph style CTParagraphStyleSetting paragraphStyleSettings[] = { { .spec = kCTParagraphStyleSpecifierAlignment, .valueSize = sizeof ctTextAlignment, .value = &ctTextAlignment } }; CFIndex settingCount = sizeof paragraphStyleSettings / sizeof *paragraphStyleSettings; CTParagraphStyleRef style = CTParagraphStyleCreate(paragraphStyleSettings, settingCount); //create two fonts, with the same name but differing font sizes CTFontRef dropCapFontRef = CTFontCreateWithName((__bridge CFStringRef)_fontName, _dropCapFontSize, NULL); CTFontRef textFontRef = CTFontCreateWithName((__bridge CFStringRef)_fontName, _textFontSize, NULL); //create a dictionary of style elements for the drop cap letter NSDictionary *dropCapDict = [NSDictionary dictionaryWithObjectsAndKeys: (__bridge id)dropCapFontRef, kCTFontAttributeName, _textColor.CGColor, kCTForegroundColorAttributeName, style, kCTParagraphStyleAttributeName, @(_dropCapKernValue) , kCTKernAttributeName, nil]; //convert it to a CFDictionaryRef CFDictionaryRef dropCapAttributes = (__bridge CFDictionaryRef)dropCapDict; //create a dictionary of style elements for the main text body NSDictionary *textDict = [NSDictionary dictionaryWithObjectsAndKeys: (__bridge id)textFontRef, kCTFontAttributeName, _textColor.CGColor, kCTForegroundColorAttributeName, style, kCTParagraphStyleAttributeName, nil]; //convert it to a CFDictionaryRef CFDictionaryRef textAttributes = (__bridge CFDictionaryRef)textDict; //clean up, because the dictionaries now have copies CFRelease(dropCapFontRef); CFRelease(textFontRef); CFRelease(style); //create an attributed string for the dropcap CFAttributedStringRef dropCapString = CFAttributedStringCreate(kCFAllocatorDefault, (__bridge CFStringRef)[_text substringToIndex:1], dropCapAttributes); //create an attributed string for the text body CFAttributedStringRef textString = CFAttributedStringCreate(kCFAllocatorDefault, (__bridge CFStringRef)[_text substringFromIndex:1], textAttributes); //create an frame setter for the dropcap CTFramesetterRef dropCapSetter = CTFramesetterCreateWithAttributedString(dropCapString); //create an frame setter for the dropcap CTFramesetterRef textSetter = CTFramesetterCreateWithAttributedString(textString); //clean up CFRelease(dropCapString); CFRelease(textString); //get the size of the drop cap letter CFRange range; CGSize maxSizeConstraint = CGSizeMake(200.0f, 200.0f); CGSize dropCapSize = CTFramesetterSuggestFrameSizeWithConstraints(dropCapSetter, CFRangeMake(0, 1), dropCapAttributes, maxSizeConstraint, &range); //create the path that the main body of text will be drawn into //i create the path based on the dropCapSize //adjusting to tighten things up (e.g. the *0.8,done by eye) //to get some padding around the edges of the screen //you could go to +5 (x) and self.frame.size.width -5 (same for height) CGMutablePathRef textBox = CGPathCreateMutable(); CGPathMoveToPoint(textBox, nil, dropCapSize.width, 0); CGPathAddLineToPoint(textBox, nil, dropCapSize.width, dropCapSize.height * 0.8); CGPathAddLineToPoint(textBox, nil, 0, dropCapSize.height * 0.8); CGPathAddLineToPoint(textBox, nil, 0, self.frame.size.height); CGPathAddLineToPoint(textBox, nil, self.frame.size.width, self.frame.size.height); CGPathAddLineToPoint(textBox, nil, self.frame.size.width, 0); CGPathCloseSubpath(textBox); //create a transform which will flip the CGContext into the same orientation as the UIView CGAffineTransform flipTransform = CGAffineTransformIdentity; flipTransform = CGAffineTransformTranslate(flipTransform, 0, self.bounds.size.height); flipTransform = CGAffineTransformScale(flipTransform, 1, -1); //invert the path for the text box CGPathRef invertedTextBox = CGPathCreateCopyByTransformingPath(textBox, &flipTransform); CFRelease(textBox); //create the CTFrame that will hold the main body of text CTFrameRef textFrame = CTFramesetterCreateFrame(textSetter, CFRangeMake(0, 0), invertedTextBox, NULL); CFRelease(invertedTextBox); CFRelease(textSetter); //create the drop cap text box //it is inverted already because we don''t have to create an independent cgpathref (like above) CGPathRef dropCapTextBox = CGPathCreateWithRect(CGRectMake(_dropCapKernValue/2.0f, 0, dropCapSize.width, dropCapSize.height), &flipTransform); CTFrameRef dropCapFrame = CTFramesetterCreateFrame(dropCapSetter, CFRangeMake(0, 0), dropCapTextBox, NULL); CFRelease(dropCapTextBox); CFRelease(dropCapSetter); //draw the frames into our graphic context CGContextRef gc = UIGraphicsGetCurrentContext(); CGContextSaveGState(gc); { CGContextConcatCTM(gc, flipTransform); CTFrameDraw(dropCapFrame, gc); CTFrameDraw(textFrame, gc); } CGContextRestoreGState(gc); CFRelease(dropCapFrame); CFRelease(textFrame); }

PD: esto viene con algo de inspiración de: https://.com/a/9272955/1218605


CoreText no puede hacer mayúsculas porque se compone de líneas compuestas de carreras de glifos. Una tapa de caída cubrirá varias líneas que no es compatible.

Para lograr este efecto, tendría que dibujar la tapa por separado y luego dibujar el resto del texto en una ruta que lo rodea.

Larga historia corta: no es posible en UILabel, es posible, pero sí un poco de trabajo con CoreText.

Los pasos para hacerlo con CoreText son:

  • crear un framesetter para el único carácter.
  • obtener sus límites
  • crear un camino que ahorra el marco de la tapa de caída
  • Crea una secuencia de cuadros para los caracteres restantes con esta ruta.
  • dibujar primer glifo
  • dibujar descanso

No es una solución perfecta, pero debe probar DTCoreText y renderizar su NSString normal como un formatted HTML . Dentro de HTML es posible "soltar una letra".


No, esto no se puede hacer solo con un NSAttributedString y un dibujo de cadena estándar.

Dado que el límite de caída es una propiedad de un párrafo, el CTParagraphStyle tendría que contener la información sobre el límite de caída. La única propiedad en CTParagraphStyle que afecta la sangría del inicio del párrafo es kCTParagraphStyleSpecifierFirstLineHeadIndent , pero que afecta solo a la primera línea.

Simplemente no hay manera de decirle a CTFramesetter cómo calcular los comienzos para la segunda y más filas.

La única forma es definir su propio atributo y escribir código para dibujar la cadena utilizando CTFramesetter y CTTypesetter que reconocen este atributo personalizado.


Si está utilizando un UITextView, puede usar textView.textContainer.exclusionPaths como sugiere Dannie P here .

Ejemplo en Swift:

class WrappingTextVC: UIViewController { override func viewDidLoad() { super.viewDidLoad() let textView = UITextView() textView.translatesAutoresizingMaskIntoConstraints = false textView.text = "ropcap example. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam vulputate ex. Fusce interdum ultricies justo in tempus. Sed ornare justo in purus dignissim, et rutrum diam pulvinar. Quisque tristique eros ligula, at dictum odio tempor sed. Fusce non nisi sapien. Donec libero orci, finibus ac libero ac, tristique pretium ex. Aenean eu lorem ut nulla elementum imperdiet. Ut posuere, nulla ut tincidunt viverra, diam massa tincidunt arcu, in lobortis erat ex sed quam. Mauris lobortis libero magna, suscipit luctus lacus imperdiet eu. Ut non dignissim lacus. Vivamus eget odio massa. Aenean pretium eget erat sed ornare. In quis tortor urna. Quisque euismod, augue vel pretium suscipit, magna diam consequat urna, id aliquet est ligula id eros. Duis eget tristique orci, quis porta turpis. Donec commodo ullamcorper purus. Suspendisse et hendrerit mi. Nulla pellentesque semper nibh vitae vulputate. Pellentesque quis volutpat velit, ut bibendum magna. Morbi sagittis, erat rutrum Suspendisse potenti. Nulla facilisi. Praesent libero est, tincidunt sit amet tempus id, blandit sit amet mi. Morbi sed odio nunc. Mauris lobortis elementum orci, at consectetur nisl egestas a. Pellentesque vel lectus maximus, semper lorem eget, accumsan mi. Etiam semper tellus ac leo porta lobortis." textView.backgroundColor = .lightGray textView.textColor = .black view.addSubview(textView) textView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20).isActive = true textView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20).isActive = true textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40).isActive = true let dropCap = UILabel() dropCap.text = "D" dropCap.font = UIFont.boldSystemFont(ofSize: 60) dropCap.backgroundColor = .lightText dropCap.sizeToFit() textView.addSubview(dropCap) textView.textContainer.exclusionPaths = [UIBezierPath(rect: dropCap.frame)] } }

Resultado:

Ejemplo completo en github