¿Cómo hago para que UILabel muestre texto delineado?

108

Todo lo que quiero es un borde negro de un píxel alrededor de mi texto UILabel blanco.

Llegué a subclasificar UILabel con el siguiente código, que improvisé torpemente a partir de algunos ejemplos en línea relacionados tangencialmente. Y funciona, pero es muy, muy lento (excepto en el simulador) y tampoco pude conseguir que centre el texto verticalmente (así que codifiqué temporalmente el valor y en la última línea). ¡Ahhhh!

void ShowStringCentered(CGContextRef gc, float x, float y, const char *str) {
    CGContextSetTextDrawingMode(gc, kCGTextInvisible);
    CGContextShowTextAtPoint(gc, 0, 0, str, strlen(str));
    CGPoint pt = CGContextGetTextPosition(gc);

    CGContextSetTextDrawingMode(gc, kCGTextFillStroke);

    CGContextShowTextAtPoint(gc, x - pt.x / 2, y, str, strlen(str));
}


- (void)drawRect:(CGRect)rect{

    CGContextRef theContext = UIGraphicsGetCurrentContext();
    CGRect viewBounds = self.bounds;

    CGContextTranslateCTM(theContext, 0, viewBounds.size.height);
    CGContextScaleCTM(theContext, 1, -1);

    CGContextSelectFont (theContext, "Helvetica", viewBounds.size.height,  kCGEncodingMacRoman);

    CGContextSetRGBFillColor (theContext, 1, 1, 1, 1);
    CGContextSetRGBStrokeColor (theContext, 0, 0, 0, 1);
    CGContextSetLineWidth(theContext, 1.0);

    ShowStringCentered(theContext, rect.size.width / 2.0, 12, [[self text] cStringUsingEncoding:NSASCIIStringEncoding]);
}

Solo tengo la persistente sensación de que estoy pasando por alto una forma más sencilla de hacer esto. Quizás anulando "drawTextInRect", pero parece que no puedo conseguir que drawTextInRect se doble a mi voluntad a pesar de mirarlo fijamente y fruncir el ceño muy, muy duro.

Monte Hurd
fuente
Aclaración: la lentitud es evidente en mi aplicación porque estoy animando la etiqueta cuando su valor cambia con un ligero aumento y reducción. Sin subclases, es suave, pero con el código arriba, la animación de la etiqueta es muy entrecortada. ¿Debería usar un UIWebView? Me siento tonto al hacerlo, ya que la etiqueta solo muestra un número ...
Monte Hurd
Ok, parece que el problema de rendimiento que estaba teniendo no estaba relacionado con el código de esquema, pero todavía parece que no puedo alinearlo verticalmente. pt.y siempre es cero por alguna razón.
Monte Hurd
Esto es muy lento para fuentes como Chalkduster
jjxtra

Respuestas:

164

Pude hacerlo anulando drawTextInRect:

- (void)drawTextInRect:(CGRect)rect {

  CGSize shadowOffset = self.shadowOffset;
  UIColor *textColor = self.textColor;

  CGContextRef c = UIGraphicsGetCurrentContext();
  CGContextSetLineWidth(c, 1);
  CGContextSetLineJoin(c, kCGLineJoinRound);

  CGContextSetTextDrawingMode(c, kCGTextStroke);
  self.textColor = [UIColor whiteColor];
  [super drawTextInRect:rect];

  CGContextSetTextDrawingMode(c, kCGTextFill);
  self.textColor = textColor;
  self.shadowOffset = CGSizeMake(0, 0);
  [super drawTextInRect:rect];

  self.shadowOffset = shadowOffset;

}
kprevas
fuente
3
Probé esta técnica pero los resultados no son satisfactorios. En mi iPhone 4 intenté usar este código para dibujar texto blanco con un contorno negro. Lo que obtuve fueron contornos negros en los bordes izquierdo y derecho de cada letra en el texto, pero no con contornos en la parte superior o inferior. ¿Algunas ideas?
Mike Hershberg
lo siento, estoy intentando usar su código en mi proyecto, ¡pero no pasa nada! ver esta foto: enlace
Momi
@Momeks: Tienes que UILabelcrear una subclase y poner la -drawTextInRect:implementación allí.
Jeff Kelley
1
@Momeks: Si no sabe cómo crear una subclase en Objective-C, debería buscar en Google; Apple tiene una guía para el lenguaje Objective-C.
Jeff Kelley
1
Probablemente, no creo que existiera cuando escribí esto hace 2.5 años :)
kprevas
114

Una solución más simple es usar una cadena de atributos como esta:

Rápido 4:

let strokeTextAttributes: [NSAttributedStringKey : Any] = [
    NSAttributedStringKey.strokeColor : UIColor.black,
    NSAttributedStringKey.foregroundColor : UIColor.white,
    NSAttributedStringKey.strokeWidth : -2.0,
    ]

myLabel.attributedText = NSAttributedString(string: "Foo", attributes: strokeTextAttributes)

Rápido 4.2:

let strokeTextAttributes: [NSAttributedString.Key : Any] = [
    .strokeColor : UIColor.black,
    .foregroundColor : UIColor.white,
    .strokeWidth : -2.0,
    ]

myLabel.attributedText = NSAttributedString(string: "Foo", attributes: strokeTextAttributes)

En a, UITextFieldtambién puede configurar el defaultTextAttributesy el attributedPlaceholder.

Tenga en cuenta que NSStrokeWidthAttributeName tiene que ser negativo en este caso, es decir, solo funcionan los contornos internos.

UITextFields con un esquema utilizando textos atribuidos

Axel Guilmin
fuente
17
Para mayor comodidad en Objective-C, será: label.attributedText = [[NSAttributedString alloc] initWithString: @ "String" atributos: @ {NSStrokeColorAttributeName: [UIColor blackColor], NSForegroundColorAttributeName: [UIColor whiteColor], NSStrokeWidthAttribute ;
Leszek Szary
8
El uso de este meme comunicó de manera efectiva los matices de este enfoque
woody121
Swift 4.2: let strokeTextAttributes: [String: Any] = [NSStrokeColorAttributeName: UIColor.black, NSForegroundColorAttributeName: UIColor.white, NSStrokeWidthAttributeName: -4.0,] consoleView.typingAttributes = strokeTextAttributes
Sartori
Gracias @TeoSartori, agregué la sintaxis Swift 4.2 en mi respuesta;)
Axel Guilmin
25

Después de leer la respuesta aceptada y las dos correcciones y la respuesta de Axel Guilmin, decidí compilar una solución general en Swift, que me conviene:

import UIKit

class UIOutlinedLabel: UILabel {

    var outlineWidth: CGFloat = 1
    var outlineColor: UIColor = UIColor.whiteColor()

    override func drawTextInRect(rect: CGRect) {

        let strokeTextAttributes = [
            NSStrokeColorAttributeName : outlineColor,
            NSStrokeWidthAttributeName : -1 * outlineWidth,
        ]

        self.attributedText = NSAttributedString(string: self.text ?? "", attributes: strokeTextAttributes)
        super.drawTextInRect(rect)
    }
}

Puede agregar esta clase UILabel personalizada a una etiqueta existente en Interface Builder y cambiar el grosor del borde y su color agregando Atributos de tiempo de ejecución definidos por el usuario como este: Configuración de Interface Builder

Resultado:

gran G roja con contorno

Lachezar
fuente
1
Agradable. Haré un IBInspectable con esto :)
Mário Carvalho
@Lachezar ¿cómo se puede hacer esto con un texto de UIButton?
CrazyOne
8

Hay un problema con la implementación de la respuesta. Dibujar un texto con trazo tiene un ancho de glifo de carácter ligeramente diferente al dibujo de un texto sin trazo, lo que puede producir resultados "no centrados". Se puede solucionar agregando un trazo invisible alrededor del texto de relleno.

Reemplazar:

CGContextSetTextDrawingMode(c, kCGTextFill);
self.textColor = textColor;
self.shadowOffset = CGSizeMake(0, 0);
[super drawTextInRect:rect];

con:

CGContextSetTextDrawingMode(context, kCGTextFillStroke);
self.textColor = textColor;
[[UIColor clearColor] setStroke]; // invisible stroke
self.shadowOffset = CGSizeMake(0, 0);
[super drawTextInRect:rect];

No estoy 100% seguro, si ese es el verdadero negocio, porque no sé si self.textColor = textColor;tiene el mismo efecto [textColor setFill], pero debería funcionar.

Divulgación: soy el desarrollador de THLabel.

Lancé una subclase de UILabel hace un tiempo, que permite un esquema en el texto y otros efectos. Puede encontrarlo aquí: https://github.com/tobihagemann/THLabel

tobihagemann
fuente
4
acabo de usar THLabel, la vida ya es lo suficientemente complicada como es
Prat
1
Gracias a THLabel, pude dibujar un contorno alrededor del texto agregando solo dos líneas de código. Gracias por brindar una solución a un problema que he pasado demasiado tiempo tratando de resolver.
Alan Kinnaman
Me sorprende que nadie se haya dado cuenta, pero el comando de trazo de texto incorporado no crea un contorno , sino que crea un "en línea ", es decir, el trazo se dibuja en el interior de las letras, no en el exterior. Con THLabel podemos elegir que el trazo se dibuje realmente en el exterior. ¡Gran trabajo!
lenooh
5

Esto no creará un contorno per se, pero pondrá una sombra alrededor del texto, y si hace que el radio de la sombra sea lo suficientemente pequeño, podría parecerse a un contorno.

label.layer.shadowColor = [[UIColor blackColor] CGColor];
label.layer.shadowOffset = CGSizeMake(0.0f, 0.0f);
label.layer.shadowOpacity = 1.0f;
label.layer.shadowRadius = 1.0f;

No sé si es compatible con versiones anteriores de iOS.

De todos modos, espero que ayude ...

Suran
fuente
9
Este no es un borde de un píxel alrededor de la etiqueta. Esta es una simple sombra paralela hacia el fondo.
Tim
1
esta es una solución incorrecta. Buen Dios, ¿quién lo marcó?
Sam B
dijeron con bastante claridad "perfilado", esto es una sombra. no responde la pregunta
hitme
5

Una versión de clase Swift 4 basada en la respuesta de kprevas

import Foundation
import UIKit

public class OutlinedText: UILabel{
    internal var mOutlineColor:UIColor?
    internal var mOutlineWidth:CGFloat?

    @IBInspectable var outlineColor: UIColor{
        get { return mOutlineColor ?? UIColor.clear }
        set { mOutlineColor = newValue }
    }

    @IBInspectable var outlineWidth: CGFloat{
        get { return mOutlineWidth ?? 0 }
        set { mOutlineWidth = newValue }
    }    

    override public func drawText(in rect: CGRect) {
        let shadowOffset = self.shadowOffset
        let textColor = self.textColor

        let c = UIGraphicsGetCurrentContext()
        c?.setLineWidth(outlineWidth)
        c?.setLineJoin(.round)
        c?.setTextDrawingMode(.stroke)
        self.textColor = mOutlineColor;
        super.drawText(in:rect)

        c?.setTextDrawingMode(.fill)
        self.textColor = textColor
        self.shadowOffset = CGSize(width: 0, height: 0)
        super.drawText(in:rect)

        self.shadowOffset = shadowOffset
    }
}

Se puede implementar completamente en Interface Builder configurando la clase personalizada de UILabel en OutlinedText. A continuación, tendrá la capacidad de establecer el ancho y el color del contorno desde el panel Propiedades.

ingrese la descripción de la imagen aquí

Chris Stillwell
fuente
Perfecto también para Swift 5.
Antee86
4

Si desea animar algo complicado, la mejor manera es tomar una captura de pantalla programáticamente y animarlo en su lugar.

Para tomar una captura de pantalla de una vista, necesitará un código como este:

UIGraphicsBeginImageContext(mainContentView.bounds.size);
[mainContentView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext(); 

Donde mainContentView es la vista de la que desea tomar una captura de pantalla. Agregue viewImage a un UIImageView y anímelo.

¡Espero que acelere tu animación!

norte

Nick Cartwright
fuente
Ese es un truco increíble que puedo pensar en algunos lugares para usar, y probablemente resuelva el problema de rendimiento, pero todavía tengo la esperanza de que haya una forma más simple que mi subclase de mierda para hacer que el texto de uilabel muestre un esquema. Mientras tanto, voy a jugar con tu ejemplo, ¡gracias!
Monte Hurd
Siento tu dolor. No estoy seguro de que encuentre una buena manera de hacerlo. ¡A menudo escribo montones de código para los efectos del producto y luego los fotografío (como arriba) para una animación fluida! Para el ejemplo anterior, debe incluir <QuartzCore / QuartzCore.h> en su código. ¡La mejor de las suertes! N
Nick Cartwright
4

Como mencionó MuscleRumble , el borde de la respuesta aceptada está un poco descentrado. Pude corregir esto estableciendo el ancho del trazo en cero en lugar de cambiar el color a claro.

es decir, reemplazando:

CGContextSetTextDrawingMode(c, kCGTextFill);
self.textColor = textColor;
self.shadowOffset = CGSizeMake(0, 0);
[super drawTextInRect:rect];

con:

CGContextSetTextDrawingMode(c, kCGTextFillStroke);
self.textColor = textColor;
CGContextSetLineWidth(c, 0); // set stroke width to zero
self.shadowOffset = CGSizeMake(0, 0);
[super drawTextInRect:rect];

Solo habría comentado su respuesta, pero aparentemente no soy lo suficientemente "respetable".

ufmike
fuente
2

si TODO lo que quieres es un borde negro de un píxel alrededor de mi texto UILabel blanco,

entonces creo que está haciendo el problema más difícil de lo que es ... No sé por memoria qué función 'dibujar rect / frameRect' debería usar, pero será fácil de encontrar. este método simplemente demuestra la estrategia (¡deje que la superclase haga el trabajo!):

- (void)drawRect:(CGRect)rect
{
  [super drawRect:rect];
  [context frameRect:rect]; // research which rect drawing function to use...
}
Kent
fuente
no lo entiendo. probablemente porque he estado mirando la pantalla durante aproximadamente 12 horas y no obtengo mucho en este punto ...
Monte Hurd
está subclasificando UILabel, una clase que ya dibuja texto. y la razón por la que está subclasificando es porque desea agregar un borde negro alrededor del texto. así que si dejas que la superclase dibuje el texto, entonces todo lo que tienes que hacer es dibujar el borde, ¿no?
kent
Este es un buen punto ... aunque, si quiere agregar un borde negro de 1 píxel, ¡la subclase probablemente dibujará las letras demasiado juntas! .... ¡Haría algo como lo publicado en la pregunta original y optimizaría (como se indica en mi publicación)! N
Nick Cartwright
@nickcartwright: si se da el caso de que la superclase esté haciendo lo que usted dice, puede insertar el CGRect antes de llamar a [super drawRect]. aún más fácil y seguro que reescribir la funcionalidad de la superclase.
kent
Y luego @kent dejó SO en frustración. Gran respuesta, +1.
Dan Rosenstark
1

Encontré un problema con la respuesta principal. La posición del texto no está necesariamente centrada correctamente en la ubicación de los subpíxeles, por lo que el contorno puede no coincidir con el texto. Lo arreglé usando el siguiente código, que usa CGContextSetShouldSubpixelQuantizeFonts(ctx, false):

- (void)drawTextInRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    [self.textOutlineColor setStroke];
    [self.textColor setFill];

    CGContextSetShouldSubpixelQuantizeFonts(ctx, false);

    CGContextSetLineWidth(ctx, self.textOutlineWidth);
    CGContextSetLineJoin(ctx, kCGLineJoinRound);

    CGContextSetTextDrawingMode(ctx, kCGTextStroke);
    [self.text drawInRect:rect withFont:self.font lineBreakMode:NSLineBreakByWordWrapping alignment:self.textAlignment];

    CGContextSetTextDrawingMode(ctx, kCGTextFill);
    [self.text drawInRect:rect withFont:self.font lineBreakMode:NSLineBreakByWordWrapping alignment:self.textAlignment];
}

Esto supone que definiste textOutlineColory textOutlineWidthcomo propiedades.

Robotbugs
fuente
0

Aquí está la otra respuesta para establecer el texto delineado en la etiqueta.

extension UILabel {

func setOutLinedText(_ text: String) {
    let attribute : [NSAttributedString.Key : Any] = [
        NSAttributedString.Key.strokeColor : UIColor.black,
        NSAttributedString.Key.foregroundColor : UIColor.white,
        NSAttributedString.Key.strokeWidth : -2.0,
        NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 12)
        ] as [NSAttributedString.Key  : Any]

    let customizedText = NSMutableAttributedString(string: text,
                                                   attributes: attribute)
    attributedText = customizedText
 }

}

establecer texto delineado simplemente usando el método de extensión.

lblTitle.setOutLinedText("Enter your email address or username")
Aman Pathak
fuente
0

También es posible subclasificar UILabel con la siguiente lógica:

- (void)setText:(NSString *)text {
    [self addOutlineForAttributedText:[[NSAttributedString alloc] initWithString:text]];
}

- (void)setAttributedText:(NSAttributedString *)attributedText {
    [self addOutlineForAttributedText:attributedText];
}

- (void)addOutlineForAttributedText:(NSAttributedString *)attributedText {
    NSDictionary *strokeTextAttributes = @{
                                           NSStrokeColorAttributeName: [UIColor blackColor],
                                           NSStrokeWidthAttributeName : @(-2)
                                           };

    NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithAttributedString:attributedText];
    [attrStr addAttributes:strokeTextAttributes range:NSMakeRange(0, attrStr.length)];

    super.attributedText = attrStr;
}

y si establece texto en Storyboard, entonces:

- (instancetype)initWithCoder:(NSCoder *)aDecoder {

    self = [super initWithCoder:aDecoder];
    if (self) {
        // to apply border for text from storyboard
        [self addOutlineForAttributedText:[[NSAttributedString alloc] initWithString:self.text]];
    }
    return self;
}
dólar2048
fuente
0

Si tu objetivo es algo como esto:

ingrese la descripción de la imagen aquí

Así es como lo logré: agregué una nueva labelclase personalizada como una Subvista a mi UILabel actual (inspirada en esta respuesta ).

Simplemente cópielo y péguelo en su proyecto y estará listo:

extension UILabel {
    func addTextOutline(usingColor outlineColor: UIColor, outlineWidth: CGFloat) {
        class OutlinedText: UILabel{
            var outlineWidth: CGFloat = 0
            var outlineColor: UIColor = .clear

            override public func drawText(in rect: CGRect) {
                let shadowOffset = self.shadowOffset
                let textColor = self.textColor

                let c = UIGraphicsGetCurrentContext()
                c?.setLineWidth(outlineWidth)
                c?.setLineJoin(.round)
                c?.setTextDrawingMode(.stroke)
                self.textAlignment = .center
                self.textColor = outlineColor
                super.drawText(in:rect)

                c?.setTextDrawingMode(.fill)
                self.textColor = textColor
                self.shadowOffset = CGSize(width: 0, height: 0)
                super.drawText(in:rect)

                self.shadowOffset = shadowOffset
            }
        }

        let textOutline = OutlinedText()
        let outlineTag = 9999

        if let prevTextOutline = viewWithTag(outlineTag) {
            prevTextOutline.removeFromSuperview()
        }

        textOutline.outlineColor = outlineColor
        textOutline.outlineWidth = outlineWidth
        textOutline.textColor = textColor
        textOutline.font = font
        textOutline.text = text
        textOutline.tag = outlineTag

        sizeToFit()
        addSubview(textOutline)
        textOutline.frame = CGRect(x: -(outlineWidth / 2), y: -(outlineWidth / 2),
                                   width: bounds.width + outlineWidth,
                                   height: bounds.height + outlineWidth)
    }
}

USO:

yourLabel.addTextOutline(usingColor: .red, outlineWidth: 6)

también funciona para a UIButtoncon todas sus animaciones:

yourButton.titleLabel?.addTextOutline(usingColor: .red, outlineWidth: 6)
Gasper J.
fuente
-3

¿Por qué no crea una UIView de borde de 1px en Photoshop, luego establece una UIView con la imagen y la coloca detrás de su UILabel?

Código:

UIView *myView;
UIImage *imageName = [UIImage imageNamed:@"1pxBorderImage.png"];
UIColor *tempColour = [[UIColor alloc] initWithPatternImage:imageName];
myView.backgroundColor = tempColour;
[tempColour release];

Te ahorrará subclasificar un objeto y es bastante simple de hacer.

Sin mencionar si desea hacer animación, está integrado en la clase UIView.

Brock Woolf
fuente
El texto de la etiqueta cambia y necesito que el texto en sí tenga un contorno negro de 1 píxel. (El resto del fondo de UILabel es transparente.)
Monte Hurd
Pensé que entendía cómo esto haría que cualquier texto que coloque en el UILabel se delineara, pero estaba delirantemente cansado y ahora parece que no puedo entender cómo podría lograr eso. Voy a leer sobre initWithPatternImage.
Monte Hurd
-3

Para poner un borde con bordes redondeados alrededor de un UILabel, hago lo siguiente:

labelName.layer.borderWidth = 1;
labelName.layer.borderColor = [[UIColor grayColor] CGColor];
labelName.layer.cornerRadius = 10;

(no olvide incluir QuartzCore / QuartzCore.h)

Miguel
fuente
5
Esto agrega un gran borde alrededor de toda la etiqueta, no alrededor del texto
Pavel Alexeev