Como faço o UILabel exibir texto contornado?

108

Tudo que eu quero é uma borda preta de um pixel ao redor do meu texto UILabel branco.

Cheguei ao ponto de criar uma subclasse de UILabel com o código abaixo, que remontei desajeitadamente a partir de alguns exemplos online tangencialmente relacionados. E funciona, mas é muito, muito lento (exceto no simulador) e também não consegui centralizar o texto verticalmente (por isso codifiquei o valor y na última linha temporariamente). 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]);
}

Tenho apenas a sensação incômoda de estar negligenciando uma maneira mais simples de fazer isso. Talvez substituindo "drawTextInRect", mas não consigo fazer drawTextInRect se curvar à minha vontade, apesar de olhá-lo atentamente e franzir a testa com muita força.

Monte Hurd
fonte
Esclarecimento - a lentidão é aparente em meu aplicativo porque estou animando o rótulo quando seu valor muda com um pequeno aumento e redução. Sem subclasses é suave, mas com o código acima, a animação do rótulo é bem instável. Devo apenas usar um UIWebView? Eu me sinto uma boba fazendo isso, pois o rótulo exibe apenas um único número ...
Monte Hurd
Ok, parece que o problema de desempenho que eu estava tendo não estava relacionado ao código de estrutura de tópicos, mas ainda não consigo fazer o alinhamento vertical. pt.y é sempre zero por algum motivo.
Monte Hurd
Isso é muito lento para fontes como Chalkduster
jjxtra

Respostas:

164

Consegui fazer isso substituindo 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
fonte
3
Tentei essa técnica, mas os resultados não foram nada satisfatórios. No meu iPhone 4, tentei usar este código para desenhar um texto branco com um contorno preto. O que consegui foram contornos pretos nas bordas esquerda e direita de cada letra do texto, mas não contornos na parte superior ou inferior. Alguma ideia?
Mike Hershberg
Desculpe, estou tentando usar seu código em meu projeto, mas nada acontece! veja esta foto: link
Momi
@Momeks: Você tem que criar uma subclasse UILabele colocar a -drawTextInRect:implementação lá.
Jeff Kelley
1
@Momeks: Se você não sabe como fazer uma subclasse em Objective-C, você deve pesquisar no Google; A Apple tem um guia para a linguagem Objective-C.
Jeff Kelley
1
Provavelmente - eu não acho que isso existia quando eu escrevi isto há 2,5 anos :)
kprevas
114

Uma solução mais simples é usar uma string atribuída da seguinte forma:

Swift 4:

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

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

Swift 4.2:

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

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

Em a, UITextFieldvocê pode definir o defaultTextAttributese o attributedPlaceholdertambém.

Observe que o NSStrokeWidthAttributeName deve ser negativo neste caso, ou seja, apenas os contornos internos funcionam.

UITextFields com um esboço usando textos atribuídos

Axel Guilmin
fonte
17
Para sua conveniência em Objective-C, será: label.attributedText = [[NSAttributedString] initWithString: @ "String" atributos: @ {NSStrokeColorAttributeName: [UIColor blackColor], NSForegroundColorAttributeName: [UIColor whiteColor], NSStributeAt] ;
Leszek Szary
8
O uso desse meme comunicou efetivamente a nuance dessa abordagem
woody121,
Swift 4.2: deixar strokeTextAttributes: [Cadeia: Qualquer] = [NSStrokeColorAttributeName: UIColor.black, NSForegroundColorAttributeName: UIColor.white, NSStrokeWidthAttributeName: -4,0,] consoleView.typingAttributes = strokeTextAttributes
Teo Sartori
Obrigado @TeoSartori, adicionei a sintaxe do Swift 4.2 em minha resposta;)
Axel Guilmin
25

Depois de ler a resposta aceita e as duas correções para ela e a resposta de Axel Guilmin, decidi compilar uma solução global em Swift, que me convém:

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

Você pode adicionar esta classe UILabel personalizada a um rótulo existente no Interface Builder e alterar a espessura da borda e sua cor adicionando Atributos de Tempo de Execução Definidos pelo Usuário como este: Configuração do Interface Builder

Resultado:

grande G vermelho com contorno

Lachezar
fonte
1
Agradável. Vou fazer um IBInspectável com isso :)
Mário Carvalho
@Lachezar como isso pode ser feito com um texto UIButton?
CrazyOne
8

Há um problema com a implementação da resposta. Desenhar um texto com traço tem uma largura de glifo de caractere ligeiramente diferente do desenho de um texto sem traço, o que pode produzir resultados "descentrados". Ele pode ser corrigido adicionando um traço invisível ao redor do texto de preenchimento.

Substituir:

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

com:

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

Não tenho 100% de certeza, se esse é o negócio real, porque não sei se self.textColor = textColor;tem o mesmo efeito que [textColor setFill], mas deve funcionar.

Divulgação: Sou o desenvolvedor do THLabel.

Eu lancei uma subclasse UILabel um tempo atrás, que permite um contorno no texto e outros efeitos. Você pode encontrá-lo aqui: https://github.com/tobihagemann/THLabel

tobihagemann
fonte
4
acabei de usar o THLabel, a vida já é complicada o suficiente
Prat
1
Graças ao THLabel, consegui desenhar um contorno ao redor do texto adicionando apenas duas linhas de código. Obrigado por fornecer uma solução para um problema que tenho passado muito tempo tentando resolver!
Alan Kinnaman
Estou surpreso que ninguém notou, mas o comando de traço de texto embutido não cria um contorno , mas sim um " embutido " - isto é, o traço é desenhado na parte interna das letras, não na parte externa. Com o THLabel, podemos escolher ter o traço realmente desenhado do lado de fora. Bom trabalho!
lenooh
5

Isso não criará um contorno em si, mas colocará uma sombra ao redor do texto e, se você tornar o raio da sombra pequeno o suficiente, ele poderá se parecer com um 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;

Não sei se é compatível com versões anteriores do iOS.

Enfim, espero que ajude ...

Suran
fonte
9
Esta não é uma borda de um pixel ao redor do rótulo. Esta é uma simples queda de sombra na parte inferior.
Tim
1
esta é a solução errada. Bom Deus quem marcou isso ?!
Sam B de
eles disseram claramente 'delineado', isto é uma sombra. não responde à pergunta
bateu em
5

Uma versão da classe Swift 4 baseada na resposta 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
    }
}

Ele pode ser implementado inteiramente no Interface Builder, definindo a classe personalizada do UILabel como OutlinedText. Você então poderá definir a largura e a cor do contorno no painel Propriedades.

insira a descrição da imagem aqui

Chris Stillwell
fonte
Perfeito para o Swift 5 também.
Antee86
4

Se você quiser animar algo complicado, a melhor maneira é fazer uma captura de tela programática e animar isso!

Para fazer uma captura de tela de uma visualização, você precisará de um código um pouco como este:

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

Onde mainContentView é a visualização da qual você deseja fazer uma captura de tela. Adicione viewImage a um UIImageView e anime-o.

Espero que isso acelere sua animação !!

N

Nick Cartwright
fonte
Esse é um truque incrível que posso pensar em alguns lugares para usar e provavelmente resolve o problema de desempenho, mas ainda tenho esperança de que haja uma maneira mais simples do que minha subclasse de merda para fazer o texto uilabel mostrar um contorno. Enquanto isso, vou brincar com o seu exemplo, obrigado!
Monte Hurd
Eu sinto sua dor. Não tenho certeza se você encontrará uma boa maneira de fazer isso. Costumo escrever resmas de código para efeitos de produto e, em seguida, instantâneo-los (como acima) para animação suave! Para o exemplo acima, você precisa incluir <QuartzCore / QuartzCore.h> em seu código! Boa sorte! N
Nick Cartwright
4

Como MuscleRumble mencionou, a fronteira da resposta aceita está um pouco fora do centro. Consegui corrigir isso definindo a largura do traço como zero em vez de mudar a cor para transparente.

ou seja, substituindo:

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

com:

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

Eu teria apenas comentado sobre sua resposta, mas aparentemente não sou "respeitável" o suficiente.

ufmike
fonte
2

se TUDO o que você quer é uma borda preta de um pixel ao redor do meu texto UILabel branco,

então eu acho que você está tornando o problema mais difícil do que é ... Não sei de memória qual função 'draw rect / frameRect' você deve usar, mas será fácil para você encontrar. este método apenas demonstra a estratégia (deixe a superclasse fazer o trabalho!):

- (void)drawRect:(CGRect)rect
{
  [super drawRect:rect];
  [context frameRect:rect]; // research which rect drawing function to use...
}
Kent
fonte
eu não entendo. provavelmente porque estou olhando para a tela por cerca de 12 horas e não consigo entender muito neste momento ...
Monte Hurd
você está criando uma subclasse de UILabel, uma classe que já desenha texto. e você está criando uma subclasse porque deseja adicionar uma borda preta ao redor do texto. então, se você deixar a superclasse desenhar o texto, tudo o que você precisa fazer é desenhar a borda, não?
Kent
Este é um bom ponto ... embora, se ele quiser adicionar uma borda preta de 1 pixel, a subclasse provavelmente desenhará as letras muito próximas! .... Eu faria algo como postado na pergunta original e otimizaria (conforme declarado em minha postagem)! N
Nick Cartwright
@nickcartwright: se ocorrer o caso em que a superclasse está fazendo o que você diz, você pode inserir o CGRect antes de chamar [super drawRect]. ainda mais fácil e seguro do que reescrever a funcionalidade da superclasse.
Kent
E então @kent deixou o SO frustrado. Ótima resposta, +1.
Dan Rosenstark
1

Encontrei um problema com a resposta principal. A posição do texto não é necessariamente centralizada corretamente na localização de subpixel, de modo que o contorno pode não corresponder ao texto. Eu corrigi usando o seguinte 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];
}

Isso pressupõe que você definiu textOutlineColore textOutlineWidthcomo propriedades.

Robotbugs
fonte
0

Aqui está outra resposta para definir o texto delineado no rótulo.

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
 }

}

definir texto delineado simplesmente usando o método de extensão.

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

também é possível criar uma subclasse de UILabel com a seguinte 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;
}

e se você definir o texto no Storyboard, então:

- (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ólar 2048
fonte
0

Se sua meta for algo assim:

insira a descrição da imagem aqui

Aqui está como eu consegui isso: Eu adicionei uma nova labelclasse personalizada como uma subvisualização ao meu UILabel atual (inspirado por esta resposta ).

Basta copiar e colar em seu projeto e você estará pronto para prosseguir:

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)

também funciona para um UIButtoncom todas as suas animações:

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

Por que você não cria um UIView de borda de 1px no Photoshop, em seguida define um UIView com a imagem e posiciona-o atrás do UILabel?

Código:

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

Isso vai economizar a criação de subclasses de um objeto e é bastante simples de fazer.

Sem mencionar que se você deseja fazer animação, ela está embutida na classe UIView.

Brock Woolf
fonte
O texto no rótulo muda e eu preciso que o próprio texto tenha um contorno preto de 1 pixel. (O resto do fundo do UILabel é transparente.)
Monte Hurd
Achei que entendia como isso faria com que qualquer texto que colocasse no UILabel fosse delineado, mas estava delirantemente cansado e agora não consigo entender como isso poderia fazer. Vou ler sobre initWithPatternImage.
Monte Hurd
-3

Para colocar uma borda com bordas arredondadas em torno de um UILabel, faço o seguinte:

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

(não se esqueça de incluir QuartzCore / QuartzCore.h)

Mike
fonte
5
Isso adiciona uma grande borda ao redor do rótulo inteiro, não ao redor do texto
Pavel Alexeev