Як зробити так, щоб текст на UILabel відобразив контур?


108

Все, що я хочу, - це чорна рамка на один піксель навколо білого тексту UILabel.

Я дістався до підкласифікації UILabel з кодом нижче, який я незграбно обмотав разом із кількох тангенціально пов’язаних онлайн-прикладів. І це працює, але це дуже, дуже повільно (за винятком тренажера), і я не міг змусити його центрувати текст вертикально (тому я жорстко закодував значення y в останньому рядку тимчасово). А-а-а!

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

У мене просто неприємне відчуття, що я випускаю простіший спосіб зробити це. Можливо, перекресливши "drawTextInRect", але я, здається, не можу змусити drawTextInRect схилятися до своєї волі, незважаючи на те, що пильно дивився на це і нахмурився насправді дуже важко.


Уточнення - повільність проявляється в моєму додатку, тому що я анімую мітку, коли її значення змінюється з невеликим ростом та скороченням. Без підкласингу це гладко, але з кодом над анімацією лейблу це дуже спритно. Чи варто просто використовувати UIWebView? Я відчуваю себе дурним, коли на етикетці відображається лише одне число ...
Монте Херд,

Гаразд, схоже, що у мене проблема з роботою не пов’язана з контурним кодом, але я все ще не можу змусити її вирівняти вертикально. pt.y чомусь завжди дорівнює нулю.
Монте Херд

Це дуже повільно для таких шрифтів, як Chalkduster
jjxtra

Відповіді:


164

Мені вдалося це зробити, змінивши 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;

}

3
Я спробував цю техніку, але результати не задовольняють. На своєму iPhone 4 я спробував за допомогою цього коду намалювати білий текст із чорним контуром. Я отримав чорні контури лівого та правого країв кожної літери тексту, але не контури вгорі чи внизу. Якісь ідеї?
Майк Гершберг

вибачте, я намагаюся використовувати ваш код у своєму проекті, але нічого не відбувається! дивіться цю фотографію: посилання
Momi

@Momeks: Ви повинні підклас UILabelі поставити -drawTextInRect:там реалізацію.
Джефф Келлі

1
@Momeks: якщо ви не знаєте, як скласти підклас в Objective-C, вам слід зробити Google. Apple має посібник з мови Objective-C.
Джефф Келлі

1
Можливо - я не думаю, що це існувало, коли я писав це 2,5 роки тому :)
kprevas

114

Більш простим рішенням є використання Attributed String так:

Швидкий 4:

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

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

Швидкий 4.2:

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

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

На a UITextFieldви можете встановити defaultTextAttributesі the attributedPlaceholder, і також.

Зауважте, що у цьому випадку значення NSStrokeWidthAttributeName має бути негативним , тобто працюють лише внутрішні контури.

Поля UITextFi з контуром з використанням атрибутивних текстів


17
Для зручності в Objective-C це буде: label.attributedText = [[NSAttributedString alloc] initWithString: @ "String" атрибути: @ {NSStrokeColorAttributeName: [UIColor blackColor], NSForegroundColorAttributeName: [UIColor whiteColor_CoNet], NitroTorner @CocketNet] ;
Лешек Сзарі

8
Використання цього мему ефективно
передало

Swift 4.2: нехай strokeTextAttributes: [Рядок: Будь-] = [NSStrokeColorAttributeName: UIColor.black, NSForegroundColorAttributeName: UIColor.white, NSStrokeWidthAttributeName: -4,0,] consoleView.typingAttributes = strokeTextAttributes
Тео Сарторі

Дякую @TeoSartori, я додав у відповідь синтаксис Swift 4.2;)
Axel Guilmin

25

Прочитавши прийняту відповідь та два виправлення до неї та відповідь Акселя Гільміна, я вирішив скласти загальне рішення у Swift, яке мені підходить:

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

Ви можете додати цей спеціальний клас UILabel до існуючої мітки в Builder інтерфейсу та змінити товщину межі та її колір, додавши визначені користувачем атрибути, такі як: Налаштування інтерфейсу Builder

Результат:

великий червоний G з контуром


1
Приємно. Я зроблю IBI непридатним для цього :)
Маріо Карвальо

@Lachezar, як це можна зробити з текстом UIButton?
CrazyOne

8

Є питання щодо реалізації відповіді. Малювання тексту з обведенням має дещо іншу ширину гліфів символів, ніж малювання тексту без обведення, що може давати "нецензуровані" результати. Це можна виправити, додавши невидимий штрих навколо тексту заливки.

Замінити:

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

з:

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

Я не впевнений на 100%, якщо це справжня угода, тому що я не знаю, чи self.textColor = textColor;має такий же ефект, як [textColor setFill], але це має працювати.

Розкриття інформації: Я розробник THLabel.

Нещодавно я випустив підклас UILabel, який дозволяє окреслити текст та інші ефекти. Ви можете знайти його тут: https://github.com/tobihagemann/THLabel


4
щойно використаний THLabel, життя досить складна
Prat

1
Завдяки THLabel, я зміг намалювати контур навколо тексту, додавши лише два рядки коду. Дякуємо, що ви вирішили проблему, яку я витрачав занадто довго, намагаючись вирішити!
Алан Кіннаман

Я здивований, що ніхто не помітив, але вбудована команда обведення тексту не створює контур , а натомість створює " вбудований " - тобто обведення намальовано з внутрішньої сторони літер, а не з зовнішньої сторони. За допомогою THLabel ми можемо вибрати, щоб обведення насправді було намальовано зовні. Чудова робота!
lenooh

5

Це не створить контур окремо, але він додасть тінь навколо тексту, і якщо зробити радіус тіні досить малим, він може нагадувати контур.

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

Я не знаю, чи сумісний він із старими версіями iOS ..

У будь-якому випадку, я сподіваюся, що це допоможе ...


9
Це не одна піксельна межа навколо мітки. Це проста тінь від краплі донизу.
Тім

1
це неправильне рішення. Добрий бог, який це відзначив ?!
Сем Б

вони сказали досить чітко, «окреслено», це тінь. не відповідає на питання
hitme

5

Версія класу Swift 4, заснована на відповіді 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
    }
}

Її можна повністю реалізувати в Builder інтерфейсів, встановивши для користувацького класу UILabel значення OutlinedText. Потім ви зможете встановити ширину та колір контуру на панелі властивостей.

введіть тут опис зображення


Ідеально підходить для Swift 5.
Antee86

4

Якщо ви хочете оживити щось складне, найкращий спосіб - це програмно зробити скріншот анімації, а замість цього!

Щоб зробити знімок екрана, вам знадобиться такий код:

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

Там, де mainContentView - це вигляд, для якого потрібно зробити знімок екрана. Додайте viewImage до UIImageView та анімуйте це.

Сподіваюся, що пришвидшить вашу анімацію !!

N


Це приголомшливий трюк, який я можу придумати з декількох місць, які можна використати, і, ймовірно, вирішує питання щодо продуктивності, але я все ще сподіваюсь, що існує простіший спосіб, ніж мій хитрий підклас, щоб текст uilabel показав контур. Тим часом я зіграю з вашим прикладом спасибі!
Монте Херд

Я відчуваю твій біль. Я не впевнений, що ви знайдете хороший спосіб для цього. Я часто пишу копії коду до ефектів продукту, а потім знімаю їх (як вище) для плавної анімації! Для прикладу вище вам потрібно включити <QuartzCore / QuartzCore.h> у свій код! Удачі! N
Нік Картрайт

4

Як згадував MuscleRumble , межа прийнятої відповіді трохи від центру. Я зміг виправити це, встановивши ширину обведення на нуль, замість зміни кольору на прозорий.

тобто заміна:

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

з:

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

Я б просто прокоментував його відповідь, але, мабуть, я недостатньо "поважний".


2

якщо ВСЕ, що ви хочете, є чорною облямівкою в один піксель навколо мого білого тексту UILabel,

то я думаю, що ти ускладнюєш проблему, ніж це ... Я не пам'ятаю на пам'ять, яку функцію 'draw rect / frameRect' ви повинні використовувати, але вам це буде легко знайти. цей метод просто демонструє стратегію (нехай суперклас виконує роботу!):

- (void)drawRect:(CGRect)rect
{
  [super drawRect:rect];
  [context frameRect:rect]; // research which rect drawing function to use...
}

я не розумію. напевно, тому, що я дивився на екран близько 12 годин, і мені в цьому моменті не дуже багато ...
Монте Херд

ви підкласифікуєте UILabel, клас, який вже малює текст. а причина, яку ви підкласифікуєте, полягає в тому, що ви хочете додати чорну облямівку навколо тексту. тож якщо ви дозволяєте суперкласу малювати текст, то все, що вам потрібно зробити, це намалювати межу, ні?
кент

Це хороший момент ... хоча, якщо він хоче додати чорну рамку в 1 піксель, підклас, ймовірно, намалює букви занадто близько! .... Я б робив щось на зразок розміщеного в оригінальному запитанні та оптимізував (як зазначено в моєму дописі)! N
Нік Картрайт

@nickcartwright: якщо трапляється так, що надклас робить так, як ви говорите, ви можете вставити CGRect перед тим, як викликати [super drawRect]. все ж простіше і безпечніше, ніж переписати функціональність суперкласу.
Кент

А потім @kent від розчарування покинув SO. Чудова відповідь, +1.
Дан Розенстарк

1

Я знайшов питання з основною відповіддю. Положення тексту необов'язково орієнтоване правильно на розташування субпікселів, щоб контур міг бути невідповідним навколо тексту. Я виправив його за допомогою наступного коду, який використовує 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];
}

Це передбачає, що ви визначили textOutlineColorі textOutlineWidthяк властивості.


0

Ось ще одна відповідь на викладення тексту на етикетці.

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
 }

}

встановити окреслений текст просто за допомогою методу розширення.

lblTitle.setOutLinedText("Enter your email address or username")

0

можливо також підклас UILabel за такою логікою:

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

і якщо ви встановите текст у дошці розкадрувань, виконайте вказані нижче дії.

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

0

Якщо ваша мета щось подібне:

введіть тут опис зображення

Ось як я цього досяг: я додав новий labelспеціальний клас як підвід до свого поточного UILabel (натхненний цією відповіддю ).

Просто скопіюйте та вставте його у свій проект, і ви готові:

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

ВИКОРИСТАННЯ:

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

він також працює для a UIButtonз усіма його анімаціями:

yourButton.titleLabel?.addTextOutline(usingColor: .red, outlineWidth: 6)

-3

Чому б вам не створити 1px рамку UIView в Photoshop, а потім встановити UIView із зображенням і не розмістити його за своїм UILabel?

Код:

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

Збереже вас підкласифікація об'єкта, і це зробити досить просто.

Не кажучи вже про те, що ви хочете робити анімацію, вона вбудована в клас UIView.


Текст на етикетці змінюється, і мені потрібно, щоб сам текст мав чорний контур у 1 піксель. (Решта фон UILabel прозорий.)
Монте Херд

Я думав, що розумію, як це спричинить викладення будь-якого тексту, який я розміщую в UILabel, але я був несамовито стомлений, і тепер я не можу думати про те, як це можна досягти. Я збираюся читати на initWithPatternImage.
Монте Херд

-3

Щоб поставити рамку із закругленими краями навколо UILabel, я виконую наступні дії:

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

(не забудьте включити QuartzCore / QuartzCore.h)


5
Це додає великої межі навколо цілої етикетки, а не навколо тексту
Павло Алексєєв
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.