Як зробити так, щоб текст на 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.