在Swift中根据字符串确定UILabel的大小

222

我正在尝试根据不同字符串长度计算UILabel的高度。

func calculateContentHeight() -> CGFloat{
    var maxLabelSize: CGSize = CGSizeMake(frame.size.width - 48, CGFloat(9999))
    var contentNSString = contentText as NSString
    var expectedLabelSize = contentNSString.boundingRectWithSize(maxLabelSize, options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName: UIFont.systemFontOfSize(16.0)], context: nil)
    print("\(expectedLabelSize)")
    return expectedLabelSize.size.height

}
上面是我目前用来确定高度的函数,但它不起作用。非常感谢能够得到任何帮助。我更喜欢使用Swift而不是Objective C来回答。

请尝试使用以下代码解决重复问题:https://dev59.com/6VoV5IYBdhLWcg3wPctv#61887135 - Malith Kuruwita
14个回答

622

String上使用扩展

Swift 3

extension String {
    func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
    
        return ceil(boundingBox.height)
    }

    func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)

        return ceil(boundingBox.width)
    }
}

还有关于NSAttributedString的内容(在某些情况下非常有用)

extension NSAttributedString {
    func height(withConstrainedWidth width: CGFloat) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, context: nil)
    
        return ceil(boundingBox.height)
    }

    func width(withConstrainedHeight height: CGFloat) -> CGFloat {
        let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
        let boundingBox = boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, context: nil)
    
        return ceil(boundingBox.width)
    }
}

Swift 4和5

只需更改extension String方法中attributes的值即可。

来自:

[NSFontAttributeName: font]

[.font : font]

2
@CodyWeaver,请检查widthWithConstrainedHeight方法的编辑内容。 - Kaan Dedeoglu
9
这段话的意思是询问在使用"numberOfLines" = 0(可能是UILabel特有属性,不太确定)或lineBreakMode ByWordWrapping这样的动态高度字符串时,应该如何解决问题。作者尝试将其添加到属性中,像这样[NSFontAttributeName: font, NSLineBreakMode: .ByWordWrapping],但并没有奏效。 - Francisc0
4
我想我已经找到了答案。我需要使用 NSParagraphStyleAttributeName: style,其中styleNSMutableParagraphStyle - Francisc0
5
否则我将得到编译错误,需要将'boundingRect'替换为'self.boundingRect'。 - Mike M
1
使用此答案时,在UICollectionViewsizeForItemAtIndexPath中发现一个问题,它似乎会覆盖insetForSectionAt的返回。 - Zack Shapiro
显示剩余12条评论

23

这是我使用的一个简单解决方案......与其他一些帖子中发布的解决方案类似,但它不需要 sizeToFit

请注意,这是用 Swift 5 编写的

let lbl = UILabel()
lbl.numberOfLines = 0
lbl.font = UIFont.systemFont(ofSize: 12) // make sure you set this correctly 
lbl.text = "My text that may or may not wrap lines..."

let width = 100.0 // the width of the view you are constraint to, keep in mind any applied margins here
 
let height = lbl.systemLayoutSizeFitting(CGSize(width: width, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel).height

这个处理了换行等问题。代码可能不是最优雅的,但它完成了工作。


2
非常好的回答!谢谢。 - Shawn Frank

22

对于多行文本,这个答案不能正确工作。 你可以使用UILabel构建不同的字符串扩展。

extension String {
func height(constraintedWidth width: CGFloat, font: UIFont) -> CGFloat {
    let label =  UILabel(frame: CGRect(x: 0, y: 0, width: width, height: .greatestFiniteMagnitude))
    label.numberOfLines = 0
    label.text = self
    label.font = font
    label.sizeToFit()

    return label.frame.height
 }
}

UILabel的宽度被固定,.numberOfLines设置为0。通过添加文本并调用.sizeToFit(),它会自动调整到正确的高度。

代码是使用Swift 3编写的。


20
sizeToFit 由于需要进行多次绘制,会引入许多性能问题。手动计算大小在资源方面更加便宜。 - Marco Pappalardo
2
这个解决方案应该设置UILabel的UIFont以确保正确的高度。 - Matthew Spencer
对我来说完美地工作 - 甚至考虑到空字符串(不像被接受的答案)。非常方便用于计算具有自动高度单元格的tableView的高度! - Hendies
我发现被采纳的答案适用于固定宽度,但不适用于固定高度。对于固定高度,它只会增加宽度以适应一行上的所有内容,除非文本中有换行符。这是我的备选答案:我的答案 - MkSMC
2
我发布了一个类似的解决方案--不需要调用sizeToFit - RyanG
这个解决方案适用于iOS上的任何可用字体吗?当某些特定字体返回的高度太大(1个空行)时,我遇到了一些严重的问题。 - sabiland

7
这是我在Swift 4.1和Xcode 9.4.1中的答案。
//This is your label
let proNameLbl = UILabel(frame: CGRect(x: 0, y: 20, width: 300, height: height))
proNameLbl.text = "This is your text"
proNameLbl.font = UIFont.systemFont(ofSize: 17)
proNameLbl.numberOfLines = 0
proNameLbl.lineBreakMode = .byWordWrapping
infoView.addSubview(proNameLbl)

//Function to calculate height for label based on text
func heightForView(text:String, font:UIFont, width:CGFloat) -> CGFloat {
    let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
    label.numberOfLines = 0
    label.lineBreakMode = NSLineBreakMode.byWordWrapping
    label.font = font
    label.text = text

    label.sizeToFit()
    return label.frame.height
}

现在你可以调用这个函数。
//Call this function
let height = heightForView(text: "This is your text", font: UIFont.systemFont(ofSize: 17), width: 300)
print(height)//Output : 41.0

6

Swift 5:

如果您有UILabel,并且boundingRect无法正常工作(我也遇到了这个问题。它总是返回1行高度),那么可以使用扩展轻松计算标签大小。

代码如下:
extension UILabel {
    func getSize(constrainedWidth: CGFloat) -> CGSize {
        return systemLayoutSizeFitting(CGSize(width: constrainedWidth, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
    }
}

你可以像这样使用它:
let label = UILabel()
label.text = "My text\nIs\nAwesome"
let labelSize = label.getSize(constrainedWidth:200.0)

我成功了 / 对我来说有效


4
extension String{

    func widthWithConstrainedHeight(_ height: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: CGFloat.greatestFiniteMagnitude, height: height)

        let boundingBox = self.boundingRect(with: constraintRect, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)

        return ceil(boundingBox.width)
    }

    func heightWithConstrainedWidth(_ width: CGFloat, font: UIFont) -> CGFloat? {
        let constraintRect = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)

        return ceil(boundingBox.height)
    }

}

2
我发现这个被采纳的答案适用于固定宽度,但对于固定高度则不起作用。对于固定高度,它只会增加宽度以适应一行中的所有内容,除非文本中有换行符。
宽度函数多次调用高度函数,但这是一个快速计算,并且我在UITable的行中使用该函数时没有注意到性能问题。
extension String {

    public func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [.font : font], context: nil)

        return ceil(boundingBox.height)
    }

    public func width(withConstrainedHeight height: CGFloat, font: UIFont, minimumTextWrapWidth:CGFloat) -> CGFloat {

        var textWidth:CGFloat = minimumTextWrapWidth
        let incrementWidth:CGFloat = minimumTextWrapWidth * 0.1
        var textHeight:CGFloat = self.height(withConstrainedWidth: textWidth, font: font)

        //Increase width by 10% of minimumTextWrapWidth until minimum width found that makes the text fit within the specified height
        while textHeight > height {
            textWidth += incrementWidth
            textHeight = self.height(withConstrainedWidth: textWidth, font: font)
        }
        return ceil(textWidth)
    }
}

1
minimumTextWrapWidth:CGFloat是什么? - Vyachaslav Gerchicov
这只是函数计算中的种子值。如果您期望宽度很大,选择较小的minimumTextWrapWidth将导致while循环进行额外的迭代。因此,最小宽度越大越好,但如果它大于实际所需宽度,则始终返回该宽度。 - MkSMC

1

我无法在Swift 5中使@KaanDedeoglu的解决方案适用于多行标签和文本视图 - 不知何故 - 所以我最终编写了一个“手动”解决方案,保持与@KaanDedeoglu答案中看到的相同的函数签名,供有兴趣的人使用。对于我的程序中的用途非常有效。

宽度

extension String {

    func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
        
        var wordBoxes = [CGSize]()
        var calculatedHeight = CGFloat.zero
        var calculatedWidth = CGFloat.zero
        
        for word in self.wordsWithWordSeparators() {
            
            let box = word.boundingRect(with: CGSize.zero, attributes: [.font: font], context: nil)
            let boxSize = CGSize(width: box.width, height: box.height)
            wordBoxes.append(boxSize)
            calculatedHeight += boxSize.height
            calculatedWidth = calculatedWidth < boxSize.width ? boxSize.width : calculatedWidth
        }
        
        while calculatedHeight > height && wordBoxes.count > 1 {
            
            var bestLineToRelocate = wordBoxes.count - 1
            
            for i in 1..<wordBoxes.count {
                
                let bestPotentialWidth = wordBoxes[bestLineToRelocate - 1].width + wordBoxes[bestLineToRelocate].width
                let thisPotentialWidth = wordBoxes[i - 1].width + wordBoxes[i].width
                
                if bestPotentialWidth > thisPotentialWidth {
                    bestLineToRelocate = i
                }
            }
            
            calculatedHeight -= wordBoxes[bestLineToRelocate].height
            wordBoxes[bestLineToRelocate - 1].width += wordBoxes[bestLineToRelocate].width
            wordBoxes.remove(at: bestLineToRelocate)
            calculatedWidth = max(wordBoxes[bestLineToRelocate - 1].width, calculatedWidth)
        }
        
        return ceil(calculatedWidth)
    }
}

高度

extension String {
    
    func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
        
        var wordBoxes = [CGSize]()
        var calculatedHeight = CGFloat.zero
        var currentLine = 0
        
        for word in self.wordsWithWordSeparators() {
            
            let box = word.boundingRect(with: CGSize.zero, attributes: [.font: font], context: nil)
            let boxSize = CGSize(width: box.width, height: box.height)
            
            if wordBoxes.isEmpty == true {
                wordBoxes.append(boxSize)
            }
            else if wordBoxes[currentLine].width + boxSize.width > width {
                wordBoxes.append(boxSize)
                currentLine += 1
            }
            else {
                wordBoxes[currentLine].width += boxSize.width
                wordBoxes[currentLine].height = max(wordBoxes[currentLine].height, boxSize.height)
            }
        }
        
        for wordBox in wordBoxes {
            calculatedHeight += wordBox.height
        }
        
        return calculatedHeight
    }
}

使用的辅助方法

extension String {
    
    // Should work with any language supported by Apple
    func wordsWithWordSeparators () -> [String] {
        
        let range = self.startIndex..<self.endIndex
        var words = [String]()
        
        self.enumerateSubstrings(in: range, options: .byWords) { (substr, substrRange, enclosingRange, stop) in
            let wordWithWordSeparators = String(self[enclosingRange])
            words.append(wordWithWordSeparators)
        }
        
        return words
    }
}

注意:这些高度和宽度的计算假定给定的标签或文本视图在执行换行时不会分割或连字符单词。如果您的情况不是这样,您只需要将单词替换为字符即可。此外,如果您处于运行时敏感环境中,可能需要考虑限制这些函数调用的速度或缓存结果,因为它们可能会根据字符串包含的单词数量有点昂贵。


1
不错的解决方案,但没有考虑到使用 \n 强制换行。 - Paul Lehn

1

在Swift 5中:

label.textRect(forBounds: label.bounds, limitedToNumberOfLines: 1)

顺便提一下,limitedToNumberOfLines的值取决于您想要的标签文本行数。


0
@IBOutlet weak var constraintTxtV: NSLayoutConstraint!
func TextViewDynamicallyIncreaseSize() {
    let contentSize = self.txtVDetails.sizeThatFits(self.txtVDetails.bounds.size)
    let higntcons = contentSize.height
    constraintTxtV.constant = higntcons
}

5
请参考 [answer] 以获取更多细节,您的答案不仅应包含代码,还应包含有关代码的解释。 - MechMK1
虽然这段代码可能回答了问题,但是提供关于为什么和/或如何回答问题的额外上下文可以提高其长期价值。 - Isma
这个答案不完整。它涉及到重要变量,其类型未知,这违背了目的。 - bitwit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接