Как сделать так, чтобы 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 по какой-то причине всегда равен нулю.
Monte Hurd
Это очень медленно для таких шрифтов, как Chalkduster
jjxtra 08

Ответы:

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;

}
kprevas
источник
3
Я пробовал эту технику, но результаты неутешительны. На своем iPhone 4 я попытался использовать этот код, чтобы нарисовать белый текст с черным контуром. Я получил черные контуры слева и справа от каждой буквы в тексте, но не контуры вверху или внизу. Любые идеи?
Майк Хершберг
извините, я пытаюсь использовать ваш код в своем проекте, но ничего не происходит! см. это фото: ссылка
Моми
@Momeks: Вы должны UILabelсоздать подкласс и поместить -drawTextInRect:туда реализацию.
Джефф Келли
1
@Momeks: Если вы не знаете, как создать подкласс в Objective-C, вам следует погуглить; У Apple есть руководство по языку Objective-C.
Джефф Келли
1
Наверное - я не думаю, что это существовало, когда я писал это 2,5 года назад :)
kprevas
114

Более простое решение - использовать строку с атрибутами следующим образом:

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)

На a UITextFieldвы также можете установить defaultTextAttributesи attributedPlaceholder.

Обратите внимание, что в этом случае значение NSStrokeWidthAttributeName должно быть отрицательным , т.е. работают только внутренние контуры.

UITextFields с контуром с использованием текстов с атрибутами

Аксель Гильмин
источник
17
Для удобства в Objective-C это будет: label.attributedText = [[NSAttributedString alloc] initWithString: @ Атрибуты "String": @ {NSStrokeColorAttributeName: [UIColor blackColor], NSForegroundColorAttributeName: [UIColidthName whiteColor], NSStrokeWall] ;
Leszek Szary
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 к существующей метке в Interface Builder и изменить толщину границы и ее цвет, добавив определяемые пользователем атрибуты времени выполнения следующим образом: Настройка Interface Builder

Результат:

большая красная буква G с контуром

Лачезар
источник
1
Ницца. Я сделаю это IBInspectable :)
Марио Карвалью
@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

Tobihagemann
источник
4
просто использовал THLabel, жизнь и так достаточно сложна
Прат
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
это неправильное решение. Боже, кто это сделал ?!
Sam B
они сказали довольно четко «очерчено», это тень. не отвечает на вопрос
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
    }
}

Его можно полностью реализовать в Интерфейсном Разработчике, установив для настраиваемого класса 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];

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

ufmike
источник
2

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

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

- (void)drawRect:(CGRect)rect
{
  [super drawRect:rect];
  [context frameRect:rect]; // research which rect drawing function to use...
}
Кент
источник
я не понимаю. вероятно, потому что я смотрел на экран около 12 часов, и сейчас я мало что понимаю ...
Монте Херд
вы создаете подкласс UILabel, класс, который уже рисует текст. и причина, по которой вы подклассифицируете, заключается в том, что вы хотите добавить черную рамку вокруг текста. так что если вы позволите суперклассу рисовать текст, тогда все, что вам нужно сделать, это нарисовать границу, не так ли?
kent
Это хороший момент ... хотя, если он хочет добавить черную рамку в 1 пиксель, подкласс, вероятно, будет рисовать буквы слишком близко друг к другу! .... Я бы сделал что-то вроде отправленного в исходный вопрос и оптимизирую (как указано в моем сообщении)! N
Ник Картрайт
@nickcartwright: если случится так, что суперкласс работает так, как вы говорите, вы можете вставить CGRect перед вызовом [super drawRect]. все же проще и безопаснее, чем переписывать функциональность суперкласса.
kent
А затем @kent ушел ТАК в отчаянии. Отличный ответ, +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;
}
доллар2048
источник
0

Если ваша цель примерно такая:

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

Вот как я этого добился: я добавил новый labelпользовательский класс в качестве Subview в мой текущий 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)

он также работает UIButtonсо всеми его анимациями:

yourButton.titleLabel?.addTextOutline(usingColor: .red, outlineWidth: 6)
Гаспер Дж.
источник
-3

Почему бы вам не создать границу UIView в 1 пиксель в 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.
Monte Hurd
-3

Чтобы поместить границу с закругленными краями вокруг UILabel, я делаю следующее:

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

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

Майк
источник
5
Это добавляет большую рамку вокруг всего лейбла, а не вокруг текста
Павел Алексеев