带有 adjustmentFontSizeToFitWidth 的多行 UILabel

发布于 2024-10-06 05:38:27 字数 396 浏览 0 评论 0原文

我有一个多行 UILabel,我想根据文本长度调整其字体大小。整个文本应适合标签的框架而不被截断。

不幸的是,根据文档,adjustsFontSizeToFitWidth 属性“仅当 numberOfLines 属性设置为 1 时才有效”。

我尝试使用确定调整后的字体大小

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

,然后减小字体大小直到适合。不幸的是,此方法在内部截断文本以适合指定的大小,并返回结果截断字符串的大小。

I have a multiline UILabel whose font size I'd like to adjust depending on the text length. The whole text should fit into the label's frame without truncating it.

Unfortunately, according to the documentation the adjustsFontSizeToFitWidth property "is effective only when the numberOfLines property is set to 1".

I tried to determine the adjusted font size using

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

and then decrementing the font size until it fits. Unfortunately, this method internally truncates the text to fit into the specified size and returns the size of the resulting truncated string.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

生活了然无味 2024-10-13 05:38:27

这个问题中,0x90 提供了一个解决方案 - 虽然有点难看- 做我想做的事。具体来说,它正确地处理了单个单词在初始字体大小下不适合宽度的情况。我稍微修改了代码,以便它作为 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;
}

将其与 UILabel 一起使用:

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

编辑:修复了代码使用 font 初始化 newFont。修复了某些情况下的崩溃问题。

In this question, 0x90 provides a solution that - although a bit ugly - does what I want. Specifically, it deals correctly with the situation that a single word does not fit the width at the initial font size. I've slightly modified the code so that it works as a category on 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;
}

To use it with a UILabel:

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

EDIT: Fixed the code to initialize newFont with font. Fixes a crash under certain circumstances.

郁金香雨 2024-10-13 05:38:27

在某些情况下,如果您知道需要多少行(例如“2”),则将“换行符”从“自动换行”更改为“截尾”可能就足够了:信用:贝基·汉斯迈尔

In some cases, changing "Line Breaks" from "Word Wrap" to "Truncate Tail" may be all you need, if you know how many lines you want (e.g. "2"): Credit: Becky Hansmeyer

枕头说它不想醒 2024-10-13 05:38:27

要获得完整的解决方案,请参阅我的答案的底部

For a fully working solution, see the bottom of my answer ????

To manually measure the dimensions of the text / attributedText of your UILabel in order to find the appropriate font size using your own strategy, you have a few options:

  1. Use NSString's size(withAttributes:) or NSAttributedString's size() function. These are only partially useful because they assume the text is one line.

  2. Use NSAttributedString's boundingRect() function, which takes a few drawing options, making sure you supply .usesLineFragmentOrigin to support multiple lines:

    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. Use TextKit and your own 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. Use CoreText, a more powerful and low-level API for laying out text. This would probably be unnecessarily complicated for this task.

Regardless of what you choose to use to measure text, you will probably need to do two passes: The first pass is to account for long words that should not be broken up over multiple lines, where you will need to find the largest font size that fits the largest (~longest) word fully within the label's bounds. In the second pass, you can continue your search downwards from the result of the first pass to find an even smaller font size as required to fit the entire text this time.

When doing the largest word measurement (rather than the entire text), you don't want to restrict the width parameter that you supply to some of the above sizing functions, otherwise the system will have no choice but to break up the single word you gave it and return incorrect results for your purposes. You will need to replace the width argument of the above methods with CGFloat.greatestFiniteMagnitude:

  • the width part of the size argument of boundingRect().
  • the width part of the size argument of NSTextContainer().

You will also need to consider the actual algorithm that you use for your search, since measuring text is an expensive operation and a linear search might be too slow, depending on your needs. If you want to be more efficient you can apply a binary search instead. ????

For a robust working solution based on the above, see my open-source framework AccessibilityKit. A couple of examples of AKLabel in action:

Example1

Example2

Hope this helps!

说谎友 2024-10-13 05:38:27

谢谢,有了这个以及其他人的更多信息,我做了这个自定义 UILabel,它将尊重最小字体大小,并且有一个额外的选项可以将文本对齐到顶部。

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

希望有帮助。

Thanks, with that and a little more from someone else I did this custom UILabel, that will respect the minimum font size and there's a bonus option to align the text to top.

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

I hope it helps.

爱的那么颓废 2024-10-13 05:38:27

在我看来,现在这个问题有了更好的答案

我发现,当您设置 adjustmentFontSizeToFitWidth true、numberOfLines 0、minimumScaleFactor 足够小以使字体根据需要缩小时,多行标签上的字体大小会自动调整,并且默认 lineBreakMode (byTruncatingTail)

PS:我在官方文档中找不到有关此更改的任何内容。我创建了一个问题来查找有关此主题的更多信息 这里

It seems to me that there is now a better answer to this question

I have discovered that the font size adjusts automatically on a multiline label when you set adjustsFontSizeToFitWidth true, numberOfLines 0, a minimumScaleFactor little enough to let the font shrink as needed, and the default lineBreakMode (byTruncatingTail)

PS: I cannot find anything about this change on the official documentation. I created a question to find out more informations on this topic here

堇色安年 2024-10-13 05:38:27

注释中提供了 ObjC 扩展,用于计算将多行文本装入 UILabel 所需的字体大小。
它是用 Swift 重写的(自 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;
    }
}

完整代码链接:https://gist.github。 com/amayatsky/e6125a2288cc2e4f1bbf

There is an ObjC extension provided in comments, that calculate fontsize required to fit multiline text into UILabel.
It was rewritten in Swift (since it is 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;
    }
}

Link to full code: https://gist.github.com/amayatsky/e6125a2288cc2e4f1bbf

北风几吹夏 2024-10-13 05:38:27

斯威夫特 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
    }
}

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
    }
}
煞人兵器 2024-10-13 05:38:27

我想我会添加我自己对答案的看法:

我认为这对其他一些答案略有改进,因为:

  1. 在循环单词时缓存以找到宽度最长的单词,以防止不必要的计算
  2. 在执行大小时使用所有标签属性计算

https://gist.github.com/chrisjrex/c571056a4b621f7099bcbd5e179f184f

注意:此解决方案应该仅当 UILabel 具有 .numberOfLines = 0.lineBreakMode = .byWordWrapping 时使用

,否则您最好使用: .adjustFontSizeForWidth = true标准 swift 库

该线程上的其他答案对我提出解决方案非常有帮助,谢谢

Thought I would add my own take on the answer:

I think it's a slight improvement on some of the other answers because:

  1. Caching while looping through words to find the longest by width, to prevent unnecessary calculations
  2. All the label attributes are used when doing size calculations

https://gist.github.com/chrisjrex/c571056a4b621f7099bcbd5e179f184f

Note: This solution should only be used when a UILabel has .numberOfLines = 0 and .lineBreakMode = .byWordWrapping

Otherwise you are better off using: .adjustFontSizeForWidth = true from the standard swift library

The other answers on this thread were very helpful to me in coming up with my solution, thank you

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文