web-dev-qa-db-fra.com

UILabel multiligne avec ajustsFontSizeToFitWidth

J'ai un UILabel multiligne dont je voudrais ajuster la taille de la police en fonction de la longueur du texte. Le texte entier doit tenir dans le cadre de l'étiquette sans le tronquer.

Malheureusement, selon la documentation, la propriété adjustsFontSizeToFitWidth "est effective uniquement lorsque la propriété numberOfLines est définie sur 1".

J'ai essayé de déterminer la taille de police ajustée à l'aide de

-[NSString (CGSize)sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode]

puis décrémenter la taille de la police jusqu'à ce qu'elle tienne. Malheureusement, cette méthode tronque en interne le texte pour l'adapter à la taille spécifiée et renvoie la taille de la chaîne tronquée résultante.

41
Ortwin Gentz

Dans cette question , 0x90 fournit une solution qui - bien qu'un peu moche - fait ce que je veux. Plus précisément, il traite correctement la situation dans laquelle un seul mot ne correspond pas à la largeur de la taille de la police initiale. J'ai légèrement modifié le code pour qu'il fonctionne comme une catégorie sur NSString:

- (CGFloat)fontSizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size {
    CGFloat fontSize = [font pointSize];
    CGFloat height = [self sizeWithFont:font constrainedToSize:CGSizeMake(size.width,FLT_MAX) lineBreakMode:UILineBreakModeWordWrap].height;
    UIFont *newFont = font;

    //Reduce font size while too large, break if no height (empty string)
    while (height > size.height && height != 0) {   
        fontSize--;  
        newFont = [UIFont fontWithName:font.fontName size:fontSize];   
        height = [self sizeWithFont:newFont constrainedToSize:CGSizeMake(size.width,FLT_MAX) lineBreakMode:UILineBreakModeWordWrap].height;
    };

    // Loop through words in string and resize to fit
    for (NSString *Word in [self componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
        CGFloat width = [Word sizeWithFont:newFont].width;
        while (width > size.width && width != 0) {
            fontSize--;
            newFont = [UIFont fontWithName:font.fontName size:fontSize];   
            width = [Word sizeWithFont:newFont].width;
        }
    }
    return fontSize;
}

Pour l'utiliser avec une UILabel:

    CGFloat fontSize = [label.text fontSizeWithFont:[UIFont boldSystemFontOfSize:15] constrainedToSize:label.frame.size];
    label.font = [UIFont boldSystemFontOfSize:fontSize];

EDIT: Correction du code pour initialiser newFont avec font. Corrige un crash dans certaines circonstances.

50
Ortwin Gentz

Dans certains cas, remplacer les "sauts de ligne" de "Retour à la ligne" par "Couper la queue" peut suffire, si vous savez combien de lignes vous souhaitez (par exemple, "2"): Crédit: Becky Hansmeyer

5
weienw

Merci, avec cela et un peu plus de quelqu'un d'autre, j'ai créé ce UILabel personnalisé, qui respecte la taille de police minimale et offre une option de bonus permettant d'aligner le texte en haut.

h:

@interface EPCLabel : UILabel {
    float originalPointSize;
    CGSize originalSize;
}

@property (nonatomic, readwrite) BOOL alignTextOnTop;
@end

m:

#import "EPCLabel.h"

@implementation EPCLabel
@synthesize alignTextOnTop;

-(void)verticalAlignTop {
    CGSize maximumSize = originalSize;
    NSString *dateString = self.text;
    UIFont *dateFont = self.font;
    CGSize dateStringSize = [dateString sizeWithFont:dateFont 
                                   constrainedToSize:CGSizeMake(self.frame.size.width, maximumSize.height)
                                       lineBreakMode:self.lineBreakMode];

    CGRect dateFrame = CGRectMake(self.frame.Origin.x, self.frame.Origin.y, self.frame.size.width, dateStringSize.height);

    self.frame = dateFrame;
}

- (CGFloat)fontSizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size {
    CGFloat fontSize = [font pointSize];
    CGFloat height = [self.text sizeWithFont:font             
                           constrainedToSize:CGSizeMake(size.width,FLT_MAX)  
                               lineBreakMode:UILineBreakModeWordWrap].height;
    UIFont *newFont = font;

    //Reduce font size while too large, break if no height (empty string)
    while (height > size.height && height != 0 && fontSize > self.minimumFontSize) { 
        fontSize--;  
        newFont = [UIFont fontWithName:font.fontName size:fontSize];   
        height = [self.text sizeWithFont:newFont  
                       constrainedToSize:CGSizeMake(size.width,FLT_MAX) 
                           lineBreakMode:UILineBreakModeWordWrap].height;
    };

    // Loop through words in string and resize to fit
    if (fontSize > self.minimumFontSize) {
        for (NSString *Word in [self.text componentsSeparatedByString:@" "]) {
            CGFloat width = [Word sizeWithFont:newFont].width;
            while (width > size.width && width != 0 && fontSize > self.minimumFontSize) {
                fontSize--;
                newFont = [UIFont fontWithName:font.fontName size:fontSize];   
                width = [Word sizeWithFont:newFont].width;
            }
        }
    }
    return fontSize;
}

-(void)setText:(NSString *)text {
    [super setText:text];
    if (originalSize.height == 0) {
        originalPointSize = self.font.pointSize;
        originalSize = self.frame.size;
    }

    if (self.adjustsFontSizeToFitWidth && self.numberOfLines > 1) {
        UIFont *origFont = [UIFont fontWithName:self.font.fontName size:originalPointSize];
        self.font = [UIFont fontWithName:origFont.fontName size:[self fontSizeWithFont:origFont constrainedToSize:originalSize]];
    }

    if (self.alignTextOnTop) [self verticalAlignTop];
}

-(void)setAlignTextOnTop:(BOOL)flag {
    alignTextOnTop = YES;
    if (alignTextOnTop && self.text != nil)
        [self verticalAlignTop];
}

@end

J'espère que ça aide.

3
Everton Cunha

Pour une solution pleinement opérationnelle, voyez le bas de ma réponse.

Pour mesurer manuellement les dimensions de text/attributedText de votre UILabel afin de trouver la taille de police appropriée à l'aide de votre propre stratégie, vous avez quelques options:

  1. Utilisez la fonction NSString's size(withAttributes:) ou NSAttributedString's size(). Celles-ci ne sont que partiellement utiles car elles supposent que le texte ne comporte qu'une ligne.

  2. Utilisez la fonction boundingRect() de NSAttributedString, qui prend quelques options de dessin, en vous assurant de fournir .usesLineFragmentOrigin pour prendre en charge plusieurs lignes:

    var textToMeasure = label.attributedText
    
    // Modify the font size in `textToMeasure` as necessary
    
    // Now measure
    let rect = textToMeasure.boundingRect(with: label.bounds, options: [. usesLineFragmentOrigin], context: nil)
    
  3. Utilisez TextKit et votre propre NSLayoutManager:

    var textToMeasure = label.attributedText
    
    // Modify the font size in `textToMeasure` as necessary
    
    // Now measure
    let layoutManager = NSLayoutManager()
    let textContainer = NSTextContainer(size: CGSize(width: label.bounds.width, height: .greatestFiniteMagnitude))
    let textStorage = NSTextStorage(attributedString: string)
    textStorage.addLayoutManager(layoutManager)
    layoutManager.addTextContainer(textContainer)
    let glyphRange = layoutManager.glyphRange(for: textContainer)
    let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
    
  4. Utilisez CoreText, une API plus puissante et de bas niveau pour la mise en page du texte. Cela serait probablement inutilement compliqué pour cette tâche.

Indépendamment de ce que vous choisissez d’utiliser pour mesurer le texte, vous devrez probablement effectuer deux passes: La première passe consiste à prendre en compte les mots longs qui ne doivent pas être séparés sur plusieurs lignes, où vous devrez rechercher la plus grande correspond au mot le plus long (~ le plus long) entièrement dans les limites de l'étiquette. Lors du second passage, vous pouvez poursuivre votre recherche à partir du résultat du premier passage, afin de trouver une taille de police encore plus petite, comme requis pour l’ensemble du texte, cette fois-ci.

Lorsque vous effectuez la mesure Word la plus grande (plutôt que le texte entier), vous ne souhaitez pas limiter le paramètre width que vous fournissez à certaines des fonctions de dimensionnement ci-dessus, sinon le système n'aura pas d'autre choix que de décomposer le seul mot l'a donné et renvoie des résultats incorrects pour vos besoins. Vous devrez remplacer l'argument width des méthodes ci-dessus par CGFloat.greatestFiniteMagnitude:

  • la partie largeur de l'argument de taille de boundingRect().
  • la partie largeur de l'argument de taille de NSTextContainer().

Vous devrez également tenir compte de l'algorithme que vous utilisez pour votre recherche, car la mesure du texte est une opération coûteuse et une recherche linéaire peut s'avérer trop lente, selon vos besoins. Si vous voulez être plus efficace, vous pouvez appliquer une recherche binaire à la place. ????

Pour une solution de travail robuste basée sur ce qui précède, voir mon framework open-source AccessibilityKit . Quelques exemples de AKLabel en action:

Example1

Example2

J'espère que cela t'aides!

2
Kosta Eleftheriou

Il existe une extension ObjC fournie dans les commentaires, qui calcule la taille de police nécessaire pour insérer du texte multiligne dans UILabel . Elle a été réécrite dans Swift (depuis 2016):

//
//  NSString+KBAdditions.Swift
//
//  Created by Alexander Mayatsky on 16/03/16.
//
//  Original code from http://stackoverflow.com/a/4383281/463892 & http://stackoverflow.com/a/18951386
//

import Foundation
import UIKit

protocol NSStringKBAdditions {
    func fontSizeWithFont(font: UIFont, constrainedToSize size: CGSize, minimumScaleFactor: CGFloat) -> CGFloat
}

extension NSString : NSStringKBAdditions {
    func fontSizeWithFont(font: UIFont, constrainedToSize size: CGSize, minimumScaleFactor: CGFloat) -> CGFloat {
        var fontSize = font.pointSize
        let minimumFontSize = fontSize * minimumScaleFactor


        var attributedText = NSAttributedString(string: self as String, attributes:[NSFontAttributeName: font])
        var height = attributedText.boundingRectWithSize(CGSize(width: size.width, height: CGFloat.max), options:NSStringDrawingOptions.UsesLineFragmentOrigin, context:nil).size.height

        var newFont = font
        //Reduce font size while too large, break if no height (empty string)
        while (height > size.height && height != 0 && fontSize > minimumFontSize) {
            fontSize--;
            newFont = UIFont(name: font.fontName, size: fontSize)!

            attributedText = NSAttributedString(string: self as String, attributes:[NSFontAttributeName: newFont])
            height = attributedText.boundingRectWithSize(CGSize(width: size.width, height: CGFloat.max), options:NSStringDrawingOptions.UsesLineFragmentOrigin, context:nil).size.height
        }

        // Loop through words in string and resize to fit
        for Word in self.componentsSeparatedByCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) {
            var width = Word.sizeWithAttributes([NSFontAttributeName:newFont]).width
            while (width > size.width && width != 0 && fontSize > minimumFontSize) {
                fontSize--
                newFont = UIFont(name: font.fontName, size: fontSize)!
                width = Word.sizeWithAttributes([NSFontAttributeName:newFont]).width
            }
        }
        return fontSize;
    }
}

Lien vers le code complet: https://Gist.github.com/amayatsky/e6125a2288cc2e4f1bbf

2
Alexander Mayatsky

Swift 4.2:

//
//  String+Utility.Swift
//
//  Created by Philip Engberg on 29/11/2018.
//  Original code from http://stackoverflow.com/a/4383281/463892 & http://stackoverflow.com/a/18951386
//

import Foundation
import UIKit

extension String {
    func fontSize(with font: UIFont, constrainedTo size: CGSize, minimumScaleFactor: CGFloat) -> CGFloat {
        var fontSize = font.pointSize
        let minimumFontSize = fontSize * minimumScaleFactor

        var attributedText = NSAttributedString(string: self, attributes: [.font: font])
        var height = attributedText.boundingRect(with: CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin], context: nil).size.height

        var newFont = font
        //Reduce font size while too large, break if no height (empty string)
        while height > size.height && height != 0 && fontSize > minimumFontSize {
            fontSize -= 1
            newFont = UIFont(name: font.fontName, size: fontSize)!

            attributedText = NSAttributedString(string: self, attributes: [.font: newFont])
            height = attributedText.boundingRect(with: CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin], context: nil).size.height
        }

        // Loop through words in string and resize to fit
        for Word in self.components(separatedBy: NSCharacterSet.whitespacesAndNewlines) {
            var width = Word.size(withAttributes: [.font: newFont]).width
            while width > size.width && width != 0 && fontSize > minimumFontSize {
                fontSize -= 1
                newFont = UIFont(name: font.fontName, size: fontSize)!
                width = Word.size(withAttributes: [.font: newFont]).width
            }
        }
        return fontSize
    }
}
0
Zappel