Jak sprawdzić, czy UILabel jest obcięty?

106

Mam plik, UILabelktóry może mieć różną długość w zależności od tego, czy moja aplikacja działa w trybie pionowym lub poziomym na iPhonie lub iPadzie. Gdy tekst jest zbyt długi, aby wyświetlić go w jednej linii i jest obcięty, chcę, aby użytkownik mógł go nacisnąć i wyświetlić wyskakujące okienko z pełnym tekstem.

Jak mogę sprawdzić, czy UILabeltekst jest obcięty? Czy to w ogóle możliwe? W tej chwili sprawdzam tylko różne długości w zależności od trybu, w którym jestem, ale nie działa to super dobrze.

Randall
źródło
1
Spójrz na rozwiązanie oparte na liczbie linii, które zamieściłem tutaj
Mikołaj
Nie mogę uwierzyć, że po tych wszystkich latach Apple wciąż nie włączyło czegoś tak podstawowego do UILabelAPI.
funct7

Odpowiedzi:

108

Możesz obliczyć szerokość łańcucha i sprawdzić, czy jest ona większa niżlabel.bounds.size.width

NSString UIKit Additions ma kilka metod obliczania rozmiaru ciągu przy użyciu określonej czcionki. Jeśli jednak masz minimalny rozmiar czcionki dla etykiety, który pozwala systemowi zmniejszyć tekst do tego rozmiaru. Możesz użyć sizeWithFont: minFontSize: actualFontSize: forWidth: lineBreakMode: w tym przypadku.

CGSize size = [label.text sizeWithAttributes:@{NSFontAttributeName:label.font}];
if (size.width > label.bounds.size.width) {
   ...
}
progrmr
źródło
1
Dzięki, właśnie tego potrzebowałem. Jedyną różnicą było to, że sizeWithFont: zwraca CGSize.
Randall
Ach, dzięki za zwrócenie uwagi, poprawiłem przykładowy kod.
progrmr
16
sizeWithFont jest przestarzałe use: [label.text sizeWithAttributes: @ {NSFontAttributeName: label.font}];
Amelia777
2
@fatuhoku numberOfLineszwraca maksymalną liczbę wierszy używanych do wyświetlania tekstu, zgodnie z opisem w UILabelodwołaniu do klasy: developer.apple.com/library/ios/documentation/UIKit/Reference/…
Paul
1
jeśli etykieta ma liczbę linii, spróbuj pomnożyć szerokość przez liczbę linii w ten sposób if (size.width> label.bounds.size.width * label.numberOfLines) {...}
Fadi Abuzant
90

Swift (jako rozszerzenie) - działa dla wielu linii uilabel:

swift4: ( attributesparametr boundingRectzmieniony nieznacznie)

extension UILabel {

    var isTruncated: Bool {

        guard let labelText = text else {
            return false
        }

        let labelTextSize = (labelText as NSString).boundingRect(
            with: CGSize(width: frame.size.width, height: .greatestFiniteMagnitude),
            options: .usesLineFragmentOrigin,
            attributes: [.font: font],
            context: nil).size

        return labelTextSize.height > bounds.size.height
    }
}

swift3:

extension UILabel {

    var isTruncated: Bool {

        guard let labelText = text else { 
            return false
        }

        let labelTextSize = (labelText as NSString).boundingRect(
            with: CGSize(width: frame.size.width, height: .greatestFiniteMagnitude),
            options: .usesLineFragmentOrigin,
            attributes: [NSFontAttributeName: font],
            context: nil).size

        return labelTextSize.height > bounds.size.height
    }
}

swift2:

extension UILabel {

    func isTruncated() -> Bool {

        if let string = self.text {

            let size: CGSize = (string as NSString).boundingRectWithSize(
                CGSize(width: self.frame.size.width, height: CGFloat(FLT_MAX)),
                options: NSStringDrawingOptions.UsesLineFragmentOrigin,
                attributes: [NSFontAttributeName: self.font],
                context: nil).size

            if (size.height > self.bounds.size.height) {
                return true
            }
        }

        return false
    }

}
Rudzik
źródło
zastąpić wysokość 999999.0 z CGFloat (FLT_MAX)
ambientlight
2
dla swift 3 użyłbym CGFloat.greatestFiniteMagnitude
zero3nna
2
fajna odpowiedź, ale lepiej jest użyć właściwości obliczonej zamiast func: var isTruncated: Bool {if let string = self.text {let size: CGSize = (string as NSString) .boundingRect (with: CGSize (width: self.frame.size .width, height: CGFloat.greatestFiniteMagnitude), opcje: NSStringDrawingOptions.usesLineFragmentOrigin, atrybuty: [NSFontAttributeName: self.font], context: nil) .size return (size.height> self.bounds.size.height)} return false}
Mohammadalijf
1
To nie zadziałało, ponieważ używałem NSAttributedString. Aby to zadziałało, musiałem zmodyfikować wartość atrybutów do użycia: attributeText? .Attributes (at: 0 ,
fficientRange
1
Świetna odpowiedź, musiałem ją trochę zmienić na moje wymagania, ale działała idealnie. Używałem również przypisanego ciągu znaków - więc dla atrybutów, których użyłem: (attributeText? .Attributes (at: 0, efektywnyRange: nil) ?? [.font: font]), po prostu upewnij się, że tekst etykiety nie jest pusty, gdy używając tego rozwiązania.
Crt Gregoric,
20

EDYCJA: Właśnie zobaczyłem, że moja odpowiedź została przegłosowana, ale fragment kodu, który podałem, jest przestarzały.
Teraz najlepszym sposobem na to jest (ARC):

NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
paragraph.lineBreakMode = mylabel.lineBreakMode;
NSDictionary *attributes = @{NSFontAttributeName : mylabel.font,
                             NSParagraphStyleAttributeName : paragraph};
CGSize constrainedSize = CGSizeMake(mylabel.bounds.size.width, NSIntegerMax);
CGRect rect = [mylabel.text boundingRectWithSize:constrainedSize
                                         options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
                                      attributes:attributes context:nil];
if (rect.size.height > mylabel.bounds.size.height) {
    NSLog(@"TOO MUCH");
}

Zwróć uwagę, że obliczony rozmiar nie jest wartością całkowitą. Więc jeśli zrobisz takie rzeczy int height = rect.size.height, stracisz część precyzji zmiennoprzecinkowej i możesz uzyskać nieprawidłowe wyniki.

Stara odpowiedź (przestarzała):

Jeśli Twoja etykieta jest wielowierszowa, możesz użyć tego kodu:

CGSize perfectSize = [mylabel.text sizeWithFont:mylabel.font constrainedToSize:CGSizeMake(mylabel.bounds.size.width, NSIntegerMax) lineBreakMode:mylabel.lineBreakMode];
if (perfectSize.height > mylabel.bounds.size.height) {
    NSLog(@"TOO MUCH");
}
Jaskółka oknówka
źródło
13

możesz stworzyć kategorię za pomocą UILabel

- (BOOL)isTextTruncated

{
    CGRect testBounds = self.bounds;
    testBounds.size.height = NSIntegerMax;
    CGRect limitActual = [self textRectForBounds:[self bounds] limitedToNumberOfLines:self.numberOfLines];
    CGRect limitTest = [self textRectForBounds:testBounds limitedToNumberOfLines:self.numberOfLines + 1];
    return limitTest.size.height>limitActual.size.height;
}
DongXu
źródło
3
z dokumentu: textRectForBounds:limitedToNumberOfLines:„Nie powinieneś bezpośrednio wywoływać tej metody” ...
Martin
@Martin dziękuję, widzę, że narzędzie tutaj jest ograniczone. Ale kiedy wywołasz tę metodę po ustawieniu granic i tekstu, zadziała dobrze
DongXu
Dlaczego dajesz +1 podczas ustawiania limitedToNumberOfLines?
Ash
@Ash, aby sprawdzić, czy etykieta jest wyższa, gdy dozwolone jest więcej miejsca na tekst.
vrwim
1
Dzięki za ten kod, zadziałał dla mnie oprócz niektórych przypadków granicznych podczas korzystania z automatycznego układu. Poprawiłem je dodając: setNeedsLayout() layoutIfNeeded()na początku metody.
Jovan Jovanovski
9

Skorzystaj z tej kategorii, aby sprawdzić, czy etykieta jest obcięta w systemie iOS 7 i nowszych.

// UILabel+Truncation.h
@interface UILabel (Truncation)

@property (nonatomic, readonly) BOOL isTruncated;

@end


// UILabel+Truncation.m
@implementation UILabel (Truncation)

- (BOOL)isTruncated
{
    CGSize sizeOfText =
      [self.text boundingRectWithSize:CGSizeMake(self.bounds.size.width, CGFLOAT_MAX)
                               options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
                            attributes:@{ NSFontAttributeName : label.font } 
                               context: nil].size;

    if (self.frame.size.height < ceilf(sizeOfText.height))
    {
        return YES;
    }
    return NO;
}

@end
Rajesh
źródło
9

Szybki 3

Możesz policzyć liczbę wierszy po przypisaniu ciągu i porównać z maksymalną liczbą wierszy etykiety.

import Foundation
import UIKit

extension UILabel {

    func countLabelLines() -> Int {
        // Call self.layoutIfNeeded() if your view is uses auto layout
        let myText = self.text! as NSString
        let attributes = [NSFontAttributeName : self.font]

        let labelSize = myText.boundingRect(with: CGSize(width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude), options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: attributes, context: nil)
        return Int(ceil(CGFloat(labelSize.height) / self.font.lineHeight))
    }

    func isTruncated() -> Bool {

        if (self.countLabelLines() > self.numberOfLines) {
            return true
        }
        return false
    }
}
Mikołaj
źródło
To dobra odpowiedź na szybkie. Dzięki.
JimmyLee
8

Aby dodać do odpowiedzi iDev , powinieneś użyć intrinsicContentSizezamiast frame, aby działał dla Autolayout

- (BOOL)isTruncated:(UILabel *)label{
        CGSize sizeOfText = [label.text boundingRectWithSize: CGSizeMake(label.intrinsicContentSize.width, CGFLOAT_MAX)
                                                     options: (NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
                                                  attributes: [NSDictionary dictionaryWithObject:label.font forKey:NSFontAttributeName] context: nil].size;

        if (self.intrinsicContentSize.height < ceilf(sizeOfText.height)) {
        return YES;
    }
    return NO;
}
onmyway133
źródło
Dzięki! Użycie intrinsicContentSize zamiast ramki było rozwiązaniem mojego problemu, gdy wysokość UILabel jest w rzeczywistości wystarczająca, aby dopasować tekst, ale ma ograniczoną liczbę linii i dlatego nadal jest obcięty.
Anton Matosov
Z jakiegoś powodu to zwraca NIE, nawet jeśli tekst nie mieści się na etykiecie
Can Poyrazoğlu
6

To jest to. Działa to z attributedText, zanim powrócimy do prostego text, co ma sens dla nas, ludzi, którzy mają do czynienia z wieloma rodzinami czcionek, rozmiarami, a nawet NSTextAttachments!

Działa dobrze z autoukładem, ale oczywiście ograniczenia muszą zostać zdefiniowane i ustawione przed sprawdzeniem isTruncated, w przeciwnym razie sama etykieta nie będzie nawet wiedziała, jak się układać, więc nie ma możliwości, aby nawet wiedziała, czy jest obcięta.

Nie działa podejście do tego problemu za pomocą zwykłego NSStringi sizeThatFits. Nie jestem pewien, jak ludzie osiągali takie pozytywne wyniki. BTW, jak już wielokrotnie wspominano, użycie sizeThatFitsnie jest wcale idealne, ponieważ bierze pod uwagę numberOfLineswynikowy rozmiar, co niweczy cały cel tego, co próbujemy zrobić, ponieważ isTruncatedzawsze wróciłoby falseniezależnie od tego, czy jest obcięty, czy nie.

extension UILabel {
    var isTruncated: Bool {
        layoutIfNeeded()

        let rectBounds = CGSize(width: bounds.width, height: .greatestFiniteMagnitude)
        var fullTextHeight: CGFloat?

        if attributedText != nil {
            fullTextHeight = attributedText?.boundingRect(with: rectBounds, options: .usesLineFragmentOrigin, context: nil).size.height
        } else {
            fullTextHeight = text?.boundingRect(with: rectBounds, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil).size.height
        }

        return (fullTextHeight ?? 0) > bounds.size.height
    }
}
Lucas
źródło
to działa dobrze dla mnie, dzięki! czy są jakieś aktualizacje lub modyfikacje?
amodkanthe
Świetna odpowiedź. Jedynym problemem jest atrybut attributeText nigdy nie jest zerowy, nawet jeśli go nie ustawisz. Zaimplementowałem to jako dwie zmienne isTextTruncated i isAttributedTextTruncated.
Harris
@Harris mówi, że wartość domyślna to zero w pliku uilabel.h
Lucas
@LucasChwe Wiem, ale w praktyce to nieprawda. Możesz spróbować samemu i przekonaj się. fyi
Harris
4

Oto wybrana odpowiedź w Swift 3 (jako rozszerzenie). OP pytał o etykiety 1 linii. Wiele szybkich odpowiedzi, które tutaj wypróbowałem, jest specyficznych dla etykiet wielowierszowych i nie jest poprawnie oznaczanych na etykietach jednowierszowych.

extension UILabel {
    var isTruncated: Bool {
        guard let labelText = text as? NSString else {
            return false
        }
        let size = labelText.size(attributes: [NSFontAttributeName: font])
        return size.width > self.bounds.width
    }
}
Travis M.
źródło
Odpowiedź Axela nie zadziałała. Ten zrobił. Dzięki!
Glenn
2

Działa to na iOS 8:

CGSize size = [label.text boundingRectWithSize:CGSizeMake(label.bounds.size.width, NSIntegerMax) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : label.font} context:nil].size;

if (size.height > label.frame.size.height) {
    NSLog(@"truncated");
}
Marcio Fonseca
źródło
2

Napisałem kategorię do pracy z obcinaniem UILabel. Działa na iOS 7 i nowszych. Mam nadzieję, że to pomoże ! Obcięcie ogona uilabel

@implementation UILabel (Truncation)

- (NSRange)truncatedRange
{
    NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:[self attributedText]];

    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
    [textStorage addLayoutManager:layoutManager];

    NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:[self bounds].size];
    textContainer.lineFragmentPadding = 0;
    [layoutManager addTextContainer:textContainer];

    NSRange truncatedrange = [layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:0];
    return truncatedrange;
}

- (BOOL)isTruncated
{
    return [self truncatedRange].location != NSNotFound;
}

- (NSString *)truncatedText
{
    NSRange truncatedrange = [self truncatedRange];
    if (truncatedrange.location != NSNotFound)
    {
        return [self.text substringWithRange:truncatedrange];
    }

    return nil;
}

@end
Alejandro Cotilla
źródło
za każdym razem daje tylko NSNotFound
Dari
2
extension UILabel {

public func resizeIfNeeded() -> CGFloat? {
    guard let text = text, !text.isEmpty else { return nil }

    if isTruncated() {
        numberOfLines = 0
        sizeToFit()
        return frame.height
    }
    return nil
}

func isTruncated() -> Bool {
    guard let text = text, !text.isEmpty else { return false }

    let size: CGSize = text.size(withAttributes: [NSAttributedStringKey.font: font])
    return size.width > self.bounds.size.width
    }
}

Możesz obliczyć szerokość ciągu i sprawdzić, czy jest ona większa niż szerokość etykiety.

Hayk Brsoyan
źródło
1

Aby dodać do tego, co zrobił @iDev , zmodyfikowałem self.frame.size.heightużycie, label.frame.size.heightale też nie użyłemNSStringDrawingUsesLineFontLeading . Po tych modyfikacjach osiągnąłem doskonałe obliczenie, kiedy nastąpi obcięcie (przynajmniej w moim przypadku).

- (BOOL)isTruncated:(UILabel *)label {
    CGSize sizeOfText = [label.text boundingRectWithSize: CGSizeMake(label.bounds.size.width, CGFLOAT_MAX)
                                                 options: (NSStringDrawingUsesLineFragmentOrigin)
                                              attributes: [NSDictionary dictionaryWithObject:label.font forKey:NSFontAttributeName] context: nil].size;

    if (label.frame.size.height < ceilf(sizeOfText.height)) {
        return YES;
    }
    return NO;
}
kgaidis
źródło
1

Miałem problemy z boundingRect(with:options:attributes:context:)używaniem autoukładu (aby ustawić maksymalną wysokość) i przypisanym tekstem zNSParagraph.lineSpacing

Odstępy między wierszami zostały zignorowane (nawet jeśli zostały przekazane attributesdo boundingRectmetody), więc etykieta mogła zostać uznana za nieobciętą, gdy była.

Rozwiązaniem, które znalazłem, jest użycie UIView.sizeThatFits:

extension UILabel {
    var isTruncated: Bool {
        layoutIfNeeded()
        let heightThatFits = sizeThatFits(bounds.size).height
        return heightThatFits > bounds.size.height
    }
}
Axel Guilmin
źródło
0

Ponieważ wszystkie powyższe odpowiedzi używają metod amortyzowanych, pomyślałem, że może to być przydatne:

- (BOOL)isLabelTruncated:(UILabel *)label
{
    BOOL isTruncated = NO;

    CGRect labelSize = [label.text boundingRectWithSize:CGSizeFromString(label.text) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : label.font} context:nil];

    if (labelSize.size.width / labelSize.size.height > label.numberOfLines) {

        isTruncated = YES;
    }

    return isTruncated;
}
Raz
źródło
Dzielisz szerokość przez wysokość i jeśli jest większa niż liczba linii (która może być równa 0), mówisz, że etykieta jest obcięta. Nie ma mowy, żeby to zadziałało.
Can Leloğlu
@ CanLeloğlu, przetestuj go. To roboczy przykład.
Raz
2
A co jeśli numberOfLines jest równe zero?
Can Leloğlu
0

Aby obsłużyć iOS 6 (tak, niektórzy z nas nadal muszą), oto kolejne rozszerzenie odpowiedzi @ iDev. Kluczową kwestią jest to, aby w iOS 6 upewnić się, że numberOfLines twojego UILabel jest ustawiona na 0 przed wywołaniem sizeThatFits; jeśli nie, da ci wynik, który mówi, że "liczba punktów do narysowania wartości numberOfLines jest potrzebna do narysowania tekstu etykiety".

- (BOOL)isTruncated
{
    CGSize sizeOfText;

    // iOS 7 & 8
    if([self.text respondsToSelector:@selector(boundingRectWithSize:options:attributes:context:)])
    {
        sizeOfText = [self.text boundingRectWithSize:CGSizeMake(self.bounds.size.width,CGFLOAT_MAX)
                                             options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
                                          attributes:@{NSFontAttributeName:self.font}
                                             context:nil].size;
    }
    // iOS 6
    else
    {
        // For iOS6, set numberOfLines to 0 (i.e. draw label text using as many lines as it takes)
        //  so that siteThatFits works correctly. If we leave it = 1 (for example), it'll come
        //  back telling us that we only need 1 line!
        NSInteger origNumLines = self.numberOfLines;
        self.numberOfLines = 0;
        sizeOfText = [self sizeThatFits:CGSizeMake(self.bounds.size.width,CGFLOAT_MAX)];
        self.numberOfLines = origNumLines;
    }

    return ((self.bounds.size.height < sizeOfText.height) ? YES : NO);
}
John Jacecko
źródło
0

Upewnij się, że wywołujesz jeden z nich w viewDidLayoutSubviews.

public extension UILabel {

    var isTextTruncated: Bool {
        layoutIfNeeded()
        return text?.boundingRect(with: CGSize(width: bounds.width, height: .greatestFiniteMagnitude), options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font!], context: nil).size.height ?? 0 > bounds.size.height
    }

    var isAttributedTextTruncated: Bool {
        layoutIfNeeded()
        return attributedText?.boundingRect(with: CGSize(width: bounds.width, height: .greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil).size.height ?? 0 > bounds.size.height
    }

}
Harris
źródło
0

SWIFT 5

Przykład dla etykiety UILabel z wieloma wierszami, która ma wyświetlać tylko 3 wiersze.

    let labelSize: CGSize = myLabel.text!.size(withAttributes: [.font: UIFont.systemFont(ofSize: 14, weight: .regular)])

    if labelSize.width > myLabel.intrinsicContentSize.width * 3 {
        // your label will truncate
    }

Chociaż użytkownik może wybrać klawisz powrotu, dodając dodatkową linię bez dodawania do "szerokości tekstu", w takim przypadku coś takiego może być również przydatne.

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {

    if text == "\n" {
        // return pressed 

    }
}
Waylan Sands
źródło
-1

Rozwiązanie Swift 3

Myślę, że najlepszym rozwiązaniem jest (1) utworzenie UILabeletykiety o takich samych właściwościach jak etykieta, którą sprawdzasz pod kątem obcięcia, (2) wywołanie .sizeToFit(), (3) porównanie atrybutów fikcyjnej etykiety z rzeczywistą etykietą.

Na przykład, jeśli chcesz sprawdzić, czy jednowierszowa etykieta o różnej szerokości jest obcięta czy nie, możesz użyć tego rozszerzenia:

extension UILabel {
    func isTruncated() -> Bool {
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: CGFloat.greatestFiniteMagnitude, height: self.bounds.height))
        label.numberOfLines = 1
        label.font = self.font
        label.text = self.text
        label.sizeToFit()
        if label.frame.width > self.frame.width {
            return true
        } else {
            return false
        }
    }
}

... ale znowu możesz łatwo zmodyfikować powyższy kod, aby dopasować go do swoich potrzeb. Powiedzmy, że twoja etykieta jest wielowierszowa i ma różną wysokość. Wtedy rozszerzenie wyglądałoby mniej więcej tak:

extension UILabel {
    func isTruncated() -> Bool {
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude))
        label.numberOfLines = 0
        label.font = self.font
        label.text = self.text
        label.sizeToFit()
        if label.frame.height > self.frame.height {
            return true
        } else {
            return false
        }
    }
}
Saoud Rizwan
źródło
-6

Czy nie byłoby łatwo ustawić atrybut tytułu dla etykiety, ustawienie tego ustawienia spowoduje wyświetlenie pełnej etykiety po najechaniu kursorem.

możesz obliczyć długość etykiety i szerokość div (przekonwertuj na długość - jQuery / Javascript - Jak przekonwertować wartość piksela (20px) na wartość liczbową (20) ).

ustaw jquery, aby ustawić tytuł, jeśli długość jest większa niż szerokość div.

var divlen = parseInt(jQuery("#yourdivid").width,10);
var lablen =jQuery("#yourlabelid").text().length;
if(lablen < divlen){
jQuery("#yourlabelid").attr("title",jQuery("#yourlabelid").text());
}
user3405326
źródło