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.
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.
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
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.
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:
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.
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)
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)
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
:
boundingRect()
.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:
J'espère que cela t'aides!
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
//
// 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
}
}