Comment faire pour qu'UILabel affiche le texte encadré?

108

Tout ce que je veux, c'est une bordure noire d'un pixel autour de mon texte UILabel blanc.

Je suis allé jusqu'à sous-classer UILabel avec le code ci-dessous, que j'ai maladroitement bricolé à partir de quelques exemples en ligne liés de manière tangentielle. Et cela fonctionne mais c'est très, très lent (sauf sur le simulateur) et je n'ai pas pu le faire centrer le texte verticalement non plus (j'ai donc codé en dur la valeur y sur la dernière ligne temporairement). 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]);
}

J'ai juste le sentiment tenace que j'oublie une façon plus simple de faire cela. Peut-être en écrasant "drawTextInRect", mais je n'arrive pas à faire en sorte que drawTextInRect se plie à ma volonté malgré le fait de le regarder attentivement et de froncer les sourcils vraiment très fort.

Monte Hurd
la source
Clarification - la lenteur est apparente dans mon application car j'anime l'étiquette lorsque sa valeur change avec une légère croissance et réduction. Sans sous-classification, c'est fluide, mais avec le code au-dessus de l'étiquette, l'animation est beaucoup plus saccadée. Dois-je simplement utiliser un UIWebView? Je me sens idiot de le faire car l'étiquette n'affiche qu'un seul numéro ...
Monte Hurd
Ok, il semble que le problème de performances que j'avais n'était pas lié au code hiérarchique, mais je n'arrive toujours pas à l'aligner verticalement. pt.y est toujours nul pour une raison quelconque.
Monte Hurd
C'est très lent pour les polices comme Chalkduster
jjxtra

Réponses:

164

J'ai pu le faire en remplaçant 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
la source
3
J'ai essayé cette technique mais les résultats sont loin d'être satisfaisants. Sur mon iPhone 4, j'ai essayé d'utiliser ce code pour dessiner du texte blanc avec un contour noir. Ce que j'ai obtenu, ce sont des contours noirs sur les bords gauche et droit de chaque lettre du texte, mais pas des contours en haut ou en bas. Des idées?
Mike Hershberg le
désolé j'essaye d'utiliser votre code sur mon projet mais rien ne se passe! voir cette photo: lien
Momi
@Momeks: Vous devez sous-classer UILabelet y mettre l' -drawTextInRect:implémentation.
Jeff Kelley
1
@Momeks: Si vous ne savez pas comment créer une sous-classe en Objective-C, vous devriez faire un peu de recherche sur Google; Apple a un guide sur le langage Objective-C.
Jeff Kelley
1
Probablement - je ne pense pas que cela existait quand j'ai écrit ceci il y a 2,5 ans :)
kprevas
114

Une solution plus simple consiste à utiliser une chaîne attribuée comme ceci:

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)

Sur a, UITextFieldvous pouvez également définir le defaultTextAttributeset le attributedPlaceholder.

Notez que le NSStrokeWidthAttributeName doit être négatif dans ce cas, c'est-à-dire que seuls les contours intérieurs fonctionnent.

UITextFields avec un contour utilisant des textes attribués

Axel Guilmin
la source
17
Pour plus de commodité en Objective-C, ce sera: label.attributedText = [[NSAttributedString alloc] initWithString: @ Attributs "String": @ {NSStrokeColorAttributeName: [UIColor blackColor], NSForegroundColorAttributeName: [UIColor whiteColor], NSStrokeWidth} ;
Leszek Szary
8
L'utilisation de ce meme communique efficacement la nuance de cette approche
woody121
Swift 4.2: laisser strokeTextAttributes: [Chaîne: Tous] = [NSStrokeColorAttributeName: UIColor.black, NSForegroundColorAttributeName: UIColor.white, NSStrokeWidthAttributeName: -4,0,] consoleView.typingAttributes = strokeTextAttributes
Teo Sartori
Merci @TeoSartori, j'ai ajouté la syntaxe Swift 4.2 dans ma réponse;)
Axel Guilmin
25

Après avoir lu la réponse acceptée et les deux corrections qui y sont apportées et la réponse d'Axel Guilmin, j'ai décidé de compiler une solution globale en Swift, qui me convient:

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

Vous pouvez ajouter cette classe UILabel personnalisée à une étiquette existante dans l'Interface Builder et modifier l'épaisseur de la bordure et sa couleur en ajoutant des attributs d'exécution définis par l'utilisateur comme ceci: Configuration d'Interface Builder

Résultat:

grand G rouge avec contour

Lachezar
la source
1
Agréable. Je vais faire un IBInspectable avec ça :)
Mário Carvalho
@Lachezar comment cela peut-il être fait avec le texte d'un UIButton?
CrazyOne
8

Il y a un problème avec la mise en œuvre de la réponse. Le dessin d'un texte avec un contour a une largeur de glyphe de caractère légèrement différente de celle d'un texte sans contour, ce qui peut produire des résultats «non centrés». Il peut être corrigé en ajoutant un trait invisible autour du texte de remplissage.

Remplacer:

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

avec:

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

Je ne suis pas sûr à 100% si c'est la vraie affaire, car je ne sais pas si cela self.textColor = textColor;a le même effet que [textColor setFill], mais cela devrait fonctionner.

Divulgation: je suis le développeur de THLabel.

J'ai publié une sous-classe UILabel il y a quelque temps, qui permet un contour dans le texte et d'autres effets. Vous pouvez le trouver ici: https://github.com/tobihagemann/THLabel

tobihagemann
la source
4
vient d'utiliser THLabel, la vie est déjà assez compliquée comme ça
Prat
1
Grâce à THLabel, j'ai pu dessiner un contour autour du texte en ajoutant seulement deux lignes de code. Merci d'avoir fourni une solution à un problème que j'ai passé trop de temps à essayer de résoudre!
Alan Kinnaman le
Je suis surpris que personne ne l'ait remarqué, mais la commande de trait de texte intégrée ne crée pas de contour , mais crée plutôt un "en ligne " - c'est-à-dire que le trait est dessiné à l'intérieur des lettres, pas à l'extérieur. Avec THLabel, nous pouvons choisir que le trait soit réellement dessiné à l'extérieur. Bon travail!
lenooh
5

Cela ne créera pas de contour en soi, mais cela mettra une ombre autour du texte, et si vous réduisez suffisamment le rayon de l'ombre, cela pourrait ressembler à un contour.

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

Je ne sais pas s'il est compatible avec les anciennes versions d'iOS.

Quoi qu'il en soit, j'espère que cela aide ...

Suran
la source
9
Ce n'est pas une bordure d'un pixel autour de l'étiquette. C'est une simple ombre portée vers le bas.
Tim
1
c'est une mauvaise solution. Bon dieu qui l'a marqué?!
Sam B
ils ont dit assez clairement «décrit», c'est une ombre. ne répond pas à la question
hitme
5

Une version de classe Swift 4 basée sur la réponse 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
    }
}

Il peut être entièrement implémenté dans Interface Builder en définissant la classe personnalisée de UILabel sur OutlinedText. Vous aurez alors la possibilité de définir la largeur et la couleur du contour à partir du volet Propriétés.

entrez la description de l'image ici

Chris Stillwell
la source
Parfait pour Swift 5 aussi.
Antee86
4

Si vous voulez animer quelque chose de compliqué, le meilleur moyen est de prendre une capture d'écran par programme et de l'animer à la place!

Pour prendre une capture d'écran d'une vue, vous aurez besoin d'un code un peu comme celui-ci:

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

Où mainContentView est la vue dont vous souhaitez prendre une capture d'écran. Ajoutez viewImage à un UIImageView et animez-le.

J'espère que cela accélère votre animation !!

N

Nick Cartwright
la source
C'est un truc génial auquel je peux penser à quelques endroits à utiliser et qui résout probablement le problème de performances, mais je garde toujours espoir qu'il existe un moyen plus simple que ma sous-classe merdique de faire en sorte que le texte uilabel montre un contour. En attendant, je vais jouer avec votre exemple merci!
Monte Hurd
Je ressens ta douleur. Je ne suis pas sûr que vous trouviez un bon moyen de le faire. J'écris souvent des rames de code sur les effets du produit, puis je les capture (comme ci-dessus) pour une animation fluide! Pour l'exemple ci-dessus, vous devez inclure <QuartzCore / QuartzCore.h> dans votre code! Bonne chance! N
Nick Cartwright
4

Comme MuscleRumble l'a mentionné, la frontière de la réponse acceptée est un peu décentrée. J'ai pu corriger cela en définissant la largeur du trait sur zéro au lieu de changer la couleur pour effacer.

c'est-à-dire en remplaçant:

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

avec:

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

J'aurais juste commenté sa réponse mais apparemment je ne suis pas assez "réputé".

ufmike
la source
2

si TOUT ce que vous voulez est une bordure noire d'un pixel autour de mon texte UILabel blanc,

alors je pense que vous rendez le problème plus difficile qu'il ne l'est ... Je ne sais pas par mémoire quelle fonction 'draw rect / frameRect' vous devriez utiliser, mais il vous sera facile de la trouver. cette méthode démontre simplement la stratégie (laissez la superclasse faire le travail!):

- (void)drawRect:(CGRect)rect
{
  [super drawRect:rect];
  [context frameRect:rect]; // research which rect drawing function to use...
}
Kent
la source
je ne comprends pas. probablement parce que je regarde l'écran depuis environ 12 heures et que je ne reçois pas grand-chose à ce stade ...
Monte Hurd
vous sous-classez UILabel, une classe qui dessine déjà du texte. et la raison pour laquelle vous sous-classez est que vous souhaitez ajouter une bordure noire autour du texte. donc si vous laissez la superclasse dessiner le texte, alors tout ce que vous avez à faire est de dessiner la bordure, non?
kent le
C'est un bon point ... même si, s'il veut ajouter une bordure noire de 1 pixel, la sous-classe dessinera probablement les lettres trop près les unes des autres! .... Je ferais quelque chose comme posté dans la question originale et optimiserais (comme indiqué dans mon message)! N
Nick Cartwright
@nickcartwright: s'il arrive que la superclasse fasse ce que vous dites, vous pouvez insérer le CGRect avant d'appeler [super drawRect]. encore plus facile et plus sûr que de réécrire la fonctionnalité de la superclasse.
kent le
Et puis @kent a quitté SO avec frustration. Excellente réponse, +1.
Dan Rosenstark
1

J'ai trouvé un problème avec la réponse principale. La position du texte n'est pas nécessairement centrée correctement sur l'emplacement du sous-pixel, de sorte que le contour peut être incohérent autour du texte. Je l'ai corrigé en utilisant le code suivant, qui utilise 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];
}

Cela suppose que vous avez défini textOutlineColoret textOutlineWidthcomme propriétés.

Robotbugs
la source
0

Voici une autre réponse pour définir le texte souligné sur l'étiquette.

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
 }

}

définir le texte encadré simplement en utilisant la méthode d'extension.

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

il est également possible de sous-classer UILabel avec la logique suivante:

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

et si vous définissez du texte dans Storyboard, alors:

- (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;
}
dollar2048
la source
0

Si votre objectif est quelque chose comme ceci:

entrez la description de l'image ici

Voici comment je l'ai réalisé: j'ai ajouté une nouvelle labelclasse personnalisée en tant que sous-vue à mon UILabel actuel (inspiré par cette réponse ).

Copiez-collez-le simplement dans votre projet et vous êtes prêt à partir:

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

USAGE:

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

ça marche aussi pour un UIButtonavec toutes ses animations:

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

Pourquoi ne pas créer une UIView de bordure 1px dans Photoshop, puis définir une UIView avec l'image et la positionner derrière votre UILabel?

Code:

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

Cela vous évitera de sous-classer un objet et c'est assez simple à faire.

Sans oublier si vous voulez faire de l'animation, elle est intégrée à la classe UIView.

Brock Woolf
la source
Le texte sur l'étiquette change et j'ai besoin que le texte lui-même ait un contour noir de 1 pixel. (Le reste de l'arrière-plan de l'UILabel est transparent.)
Monte Hurd
Je pensais avoir compris comment cela ferait en sorte que tout texte que je place dans le UILabel soit décrit, mais j'étais extrêmement fatigué et maintenant je n'arrive pas à comprendre comment cela pourrait accomplir cela. Je vais aller lire sur initWithPatternImage.
Monte Hurd
-3

Pour mettre une bordure avec des bords arrondis autour d'un UILabel, je fais ce qui suit:

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

(n'oubliez pas d'inclure QuartzCore / QuartzCore.h)

Mike
la source
5
Cela ajoute une grande bordure autour de l'étiquette entière, pas autour du texte
Pavel Alexeev