Jak sprawić, by UILabel wyświetlał konturowany tekst?

108

Wszystko, czego chcę, to czarne obramowanie jednego piksela wokół mojego białego tekstu UILabel.

Dotarłem do podklasy UILabel za pomocą poniższego kodu, który niezdarnie zebrałem razem z kilku stycznie powiązanych przykładów online. I działa, ale jest bardzo, bardzo powolny (z wyjątkiem symulatora) i nie mogłem go również wyśrodkować tekstu w pionie (więc tymczasowo zakodowałem na stałe wartość y w ostatnim wierszu). 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]);
}

Mam tylko dokuczliwe uczucie, że pomijam prostszy sposób, aby to zrobić. Być może przez nadpisanie „drawTextInRect”, ale wydaje mi się, że drawTextInRect w ogóle nie nagina się do mojej woli, mimo że wpatruję się w to uważnie i marszczy brwi.

Monte Hurd
źródło
Wyjaśnienie - powolność jest widoczna w mojej aplikacji, ponieważ animuję etykietę, gdy jej wartość zmienia się wraz z niewielkim wzrostem i kurczeniem. Bez podklas jest gładka, ale z kodem powyżej animacja etykiety jest dość nierówna. Czy powinienem po prostu użyć UIWebView? Robiąc to, czuję się głupio, ponieważ etykieta wyświetla tylko jeden numer ...
Monte Hurd
Ok, wygląda na to, że problem z wydajnością, który miałem, nie był związany z kodem konspektu, ale nadal nie mogę wyrównać go w pionie. pt.y jest zawsze z jakiegoś powodu zerowe.
Monte Hurd,
Jest to bardzo powolne w przypadku czcionek takich jak Chalkduster
jjxtra

Odpowiedzi:

164

Udało mi się to zrobić, zastępując 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
źródło
3
Wypróbowałem tę technikę, ale wyniki są mniej niż satysfakcjonujące. Na moim iPhonie 4 próbowałem użyć tego kodu, aby narysować biały tekst z czarną obwódką. Otrzymałem czarne kontury na lewej i prawej krawędzi każdej litery w tekście, ale nie kontury na górze lub na dole. Jakieś pomysły?
Mike Hershberg
przepraszam, próbuję użyć twojego kodu w moim projekcie, ale nic się nie dzieje! zobacz to zdjęcie: link
Momi
@Momeks: Musisz podklasować UILabeli umieścić -drawTextInRect:tam implementację.
Jeff Kelley,
1
@Momeks: Jeśli nie wiesz, jak utworzyć podklasę w Objective-C, powinieneś trochę googlować; Apple ma przewodnik po języku Objective-C.
Jeff Kelley,
1
Prawdopodobnie - nie sądzę, żeby istniało, kiedy to pisałem 2,5 roku temu :)
kprevas
114

Prostszym rozwiązaniem jest użycie przypisanego ciągu w następujący sposób:

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)

Na a UITextFieldmożesz również ustawić defaultTextAttributesi attributedPlaceholder.

Zauważ, że w tym przypadku wartość NSStrokeWidthAttributeName musi być ujemna , tzn. Działają tylko wewnętrzne kontury.

UITextFields z konspektem przy użyciu przypisanych tekstów

Axel Guilmin
źródło
17
Dla wygody w Objective-C będzie to: label.attributedText = [[NSAttributedString assign] initWithString: @ "String" atrybuty: @ {NSStrokeColorAttributeName: [UIColor blackColor], NSForegroundColorAttributeName: [UIColor whiteColorame ]th ;
Leszek Szary
8
Użycie tego memu skutecznie przekazało niuanse tego podejścia
woody121
Szybka 4,2 niech strokeTextAttributes [String: Każdy] = [NSStrokeColorAttributeName: UIColor.black, NSForegroundColorAttributeName: UIColor.white, NSStrokeWidthAttributeName: -4.0;] consoleView.typingAttributes = strokeTextAttributes
Teo Sartori
Dziękuję @TeoSartori, dodałem składnię Swift 4.2 w mojej odpowiedzi;)
Axel Guilmin
25

Po przeczytaniu zaakceptowanej odpowiedzi i dwóch poprawkach do niej oraz odpowiedzi od Axela Guilmina, zdecydowałem się skompilować w Swift całościowe rozwiązanie, które mi odpowiada:

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

Możesz dodać tę niestandardową klasę UILabel do istniejącej etykiety w Interface Builder i zmienić grubość obramowania i jej kolor, dodając zdefiniowane przez użytkownika atrybuty środowiska wykonawczego w następujący sposób: Konfiguracja programu Interface Builder

Wynik:

duże czerwone G z konturem

Lachezar
źródło
1
Miły. Zrobię IBInspectable z tym :)
Mário Carvalho
@Lachezar, jak można to zrobić za pomocą tekstu UIButton?
CrazyOne
8

Jest jeden problem z implementacją odpowiedzi. Rysowanie tekstu z obrysem ma nieco inną szerokość glifu niż rysowanie tekstu bez obrysu, co może dawać wyniki „bez wyśrodkowania”. Można to naprawić, dodając niewidoczny obrys wokół tekstu wypełnienia.

Zastąpić:

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

z:

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

Nie jestem w 100% pewien, czy to prawda, ponieważ nie wiem, czy self.textColor = textColor;ma taki sam efekt, jak [textColor setFill], ale powinno działać.

Ujawnienie: jestem twórcą THLabel.

Niedawno opublikowałem podklasę UILabel, która umożliwia zarys tekstu i inne efekty. Możesz go znaleźć tutaj: https://github.com/tobihagemann/THLabel

tobihagemann
źródło
4
właśnie użyłem THLabel, życie i tak jest wystarczająco skomplikowane
Prat
1
Dzięki THLabel mogłem nakreślić zarys tekstu, dodając tylko dwie linie kodu. Dziękuję za rozwiązanie problemu, który zbyt długo próbowałem rozwiązać!
Alan Kinnaman
Dziwię się, że nikt nie zauważył, ale wbudowane polecenie obrysu tekstu nie tworzy konturu , a zamiast tego tworzy „ inline ” - tzn. Obrys rysowany jest po wewnętrznej stronie liter, a nie na zewnątrz. Dzięki THLabel możemy wybrać, aby obrys był faktycznie narysowany na zewnątrz. Dobra robota!
lenooh
5

Nie spowoduje to utworzenia konturu jako takiego, ale spowoduje umieszczenie cienia wokół tekstu, a jeśli zmniejszysz promień cienia, może on przypominać kontur.

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

Nie wiem, czy jest kompatybilny ze starszymi wersjami iOS ..

W każdym razie mam nadzieję, że to pomoże ...

Suran
źródło
9
To nie jest jednopikselowa ramka wokół etykiety. To prosty cień na dno.
Tim
1
to jest złe rozwiązanie. Dobry Boże, który to oznaczył ?!
Sam B
powiedzieli dość wyraźnie „zarysowany”, to jest cień. nie odpowiada na pytanie
hitme
5

Wersja klasy Swift 4 oparta na odpowiedzi udzielonej przez 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
    }
}

Można go zaimplementować w całości w Interface Builder, ustawiając niestandardową klasę UILabel na OutlinedText. Będziesz wtedy mieć możliwość ustawienia szerokości i koloru konturu w panelu Właściwości.

wprowadź opis obrazu tutaj

Chris Stillwell
źródło
Idealny również dla Swift 5.
Antee86
4

Jeśli chcesz animować coś skomplikowanego, najlepszym sposobem jest programowe zrobienie tego zrzutu ekranu i animacji!

Aby zrobić zrzut ekranu widoku, potrzebujesz trochę takiego kodu:

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

Gdzie mainContentView to widok, z którego chcesz zrobić zrzut ekranu. Dodaj viewImage do UIImageView i animuj to.

Mam nadzieję, że to przyspieszy twoją animację !!

N

Nick Cartwright
źródło
To niesamowita sztuczka, z której mogę wymyślić kilka miejsc do użycia i prawdopodobnie rozwiązuje problem z wydajnością, ale wciąż mam nadzieję, że istnieje prostszy sposób niż moja kiepska podklasa, aby tekst uilabel pokazał zarys. W międzyczasie będę bawić się twoim przykładem, dzięki!
Monte Hurd
Czuję twój ból. Nie jestem pewien, czy znajdziesz na to dobry sposób. Często piszę mnóstwo kodu do efektów produktu, a następnie robię z nich migawkę (jak powyżej), aby uzyskać płynną animację! W powyższym przykładzie musisz dołączyć <QuartzCore / QuartzCore.h> do swojego kodu! Powodzenia! N
Nick Cartwright,
4

Jak wspomniał MuscleRumble , granica akceptowanej odpowiedzi jest nieco poza środkiem. Udało mi się to poprawić, ustawiając szerokość obrysu na zero zamiast zmieniać kolor na wyraźny.

czyli wymiana:

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

z:

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

Właśnie skomentowałbym jego odpowiedź, ale najwyraźniej nie jestem wystarczająco „renomowany”.

ufmike
źródło
2

jeśli WSZYSTKO, czego chcesz, to czarne obramowanie jednego piksela wokół mojego białego tekstu UILabel,

to myślę, że utrudniasz problem, niż jest ... Nie wiem na podstawie pamięci, której funkcji „draw rect / frameRect” powinieneś użyć, ale będzie ci łatwo ją znaleźć. ta metoda po prostu demonstruje strategię (pozwól superklasie wykonać pracę!):

- (void)drawRect:(CGRect)rect
{
  [super drawRect:rect];
  [context frameRect:rect]; // research which rect drawing function to use...
}
Kent
źródło
nie rozumiem. prawdopodobnie dlatego, że wpatruję się w ekran od około 12 godzin i nie widzę zbyt wiele w tym momencie ...
Monte Hurd
podklasujesz UILabel, klasę, która już rysuje tekst. a powodem tworzenia podklas jest to, że chcesz dodać czarną ramkę wokół tekstu. więc jeśli pozwolisz superklasie narysować tekst, wszystko, co musisz zrobić, to narysować granicę, nie?
kent
To dobra uwaga ... chociaż jeśli chce dodać czarną ramkę o wielkości 1 piksela, podklasa prawdopodobnie będzie rysować litery zbyt blisko siebie! .... Zrobiłbym coś takiego, jak zamieszczone w oryginalnym pytaniu i zoptymalizowałbym (jak stwierdzono w moim poście)! N
Nick Cartwright,
@nickcartwright: jeśli zdarzy się, że superklasa działa tak, jak mówisz, możesz wstawić CGRect przed wywołaniem [super drawRect]. wciąż łatwiejsze i bezpieczniejsze niż ponowne napisanie funkcjonalności nadklasy.
kent
A potem @kent zostawił SO w frustracji. Świetna odpowiedź, +1.
Dan Rosenstark,
1

Znalazłem problem z główną odpowiedzią. Pozycja tekstu niekoniecznie musi być prawidłowo wyśrodkowana względem lokalizacji subpikseli, więc kontur może być niedopasowany dookoła tekstu. Naprawiłem to za pomocą następującego kodu, który używa 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];
}

Zakłada się, że zdefiniowałeś textOutlineColori textOutlineWidthjako właściwości.

Roboty
źródło
0

Oto kolejna odpowiedź, aby umieścić zarysowany tekst na etykiecie.

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
 }

}

ustawić zarysowany tekst po prostu używając metody rozszerzenia.

lblTitle.setOutLinedText("Enter your email address or username")
Aman Pathak
źródło
0

możliwe jest również utworzenie podklasy UILabel z następującą logiką:

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

a jeśli ustawisz tekst w Storyboard, to:

- (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;
}
dolar2048
źródło
0

Jeśli Twoim celem jest coś takiego:

wprowadź opis obrazu tutaj

Oto, jak to osiągnąłem: Dodałem nową labelklasę niestandardową jako Widok podrzędny do mojego obecnego UILabel (zainspirowany tą odpowiedzią ).

Po prostu skopiuj i wklej go do swojego projektu i gotowe:

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

STOSOWANIE:

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

działa również UIButtonz wszystkimi jego animacjami:

yourButton.titleLabel?.addTextOutline(usingColor: .red, outlineWidth: 6)
Gasper J.
źródło
-3

Dlaczego nie utworzysz obramowania 1px UIView w Photoshopie, a następnie ustawisz UIView z obrazem i nie umieścisz go za UILabel?

Kod:

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

Zaoszczędzi ci to podklasy obiektu i jest to dość proste.

Nie wspominając o tym, że chcesz tworzyć animacje, jest to wbudowane w klasę UIView.

Brock Woolf
źródło
Tekst na etykiecie zmieni się i chcę, aby sam tekst miał 1-pikselowy czarny kontur. (Reszta tła UILabel jest przezroczysta.)
Monte Hurd
Wydawało mi się, że rozumiem, jak to spowoduje, że każdy tekst, który umieszczam w UILabel, zostanie zarysowany, ale byłem szalenie zmęczony i teraz nie mogę się skupić na tym, jak to może osiągnąć. Mam zamiar poczytać na initWithPatternImage.
Monte Hurd,
-3

Aby umieścić obramowanie z zaokrąglonymi krawędziami wokół UILabel, wykonuję następujące czynności:

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

(nie zapomnij dołączyć QuartzCore / QuartzCore.h)

mikrofon
źródło
5
To dodaje duże obramowanie wokół całej etykiety, a nie wokół tekstu
Pavel Alexeev,