简体   繁体   English

如何快速计算多行文本的最佳标签宽度

[英]How to calculate the optimal label width for multiline text in swift

I'd like to create a method to calculate the optimal width of a multi-line label to attach several labels in a horizontal row of a fixed height. 我想创建一种方法来计算多行标签的最佳宽度,以便在固定高度的水平行中附加多个标签。

With one line of text there is no problem: 一行文本没有问题:

let textAttributes: [String : Any] = [NSFontAttributeName: UIFont.preferredFont(forTextStyle: UIFontTextStyle.title2)]

let maximalWidth: CGFloat = text!.boundingRect(
        with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: height),
        options: [NSStringDrawingOptions.usesLineFragmentOrigin],
        attributes: textAttributes,
        context: nil).size.width

As far as I understood, there is no option to indicate here, that I have several lines. 据我了解,这里没有选择表明我有几行。 This method works well in other direction when we calculate the height of the text with the fixed width. 当我们计算固定宽度的文本高度时,此方法在其他方向上效果很好。 But I have the opposite goal. 但是我有相反的目标。

As a variant, I can create a label based on the longest word (to be more precise, based on the widest word, as we can have several words with the same characters count, but different rendered width): 作为一种变体,我可以基于最长的单词(更准确地说,基于最宽的单词,因为我们可以让几个单词具有相同的字符数,但是呈现的宽度不同)来创建标签:

    var sizeToReturn = CGSize()

    let maxWordsCharacterCount = text?.maxWord.characters.count
    let allLongWords: [String] = text!.wordList.filter {$0.characters.count == maxWordsCharacterCount}
    var sizes: [CGFloat] = []
    allLongWords.forEach {sizes.append($0.size(attributes: attributes).width)}
    let minimalWidth = (sizes.max()! + constantElementsWidth)

I used here two String extensions to create words list and find all longest: 我在这里使用了两个String扩展来创建单词列表并找到所有最长的单词:

extension String {
    var wordList: [String] {
    return Array(Set(components(separatedBy: .punctuationCharacters).joined(separator: "").components(separatedBy: " "))).filter {$0.characters.count > 0}
    }
}

extension String {
    var maxWord: String {
        if let max = self.wordList.max(by: {$1.characters.count > $0.characters.count}) {
        return max
    } else {return ""}
}

} }

Not a bad option, but it looks ugly if we have the text that can't be fitted in three lines and that has several short words and one long word at the end. 这不是一个不好的选择,但是如果我们的文本不能以三行显示,并且结尾处有几个短单词和一个长单词,则看起来很丑。 This long word, determined the width, will be just truncated. 确定宽度的长字将被截断。 And more of that it looks not too good with 3 short words like: 加上3个简短的单词,看起来还不太好:

  • Sell
  • the
  • car 汽车

Well, I have the minimum width, I have the maximum width. 好,我有最小宽度,我有最大宽度。 Perhaps, I can go from maximum to minimum and catch when the label starts being truncated. 也许,我可以从最大变到最小,并在标签开始被截断时捕捉到。 So I feel that there can be an elegant solution, but I'm stuck. 因此,我认为可以有一个优雅的解决方案,但我遇到了麻烦。

Hooray, I've found one of the possible solutions. 万岁,我找到了可能的解决方案之一。 You can use the code below in the playground: 您可以在操场上使用以下代码:

import UIKit
import PlaygroundSupport

//: Just a view to launch playground timeline preview
let hostView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
hostView.backgroundColor = .lightGray
PlaygroundPage.current.liveView = hostView

// MARK: - Extensions
extension String {
    var wordList: [String] {
        return Array(Set(components(separatedBy: .punctuationCharacters).joined(separator: "").components(separatedBy: " "))).filter {$0.characters.count > 0}
    }
}

extension String {
    var longestWord: String {
        if let max = self.wordList.max(by: {$1.characters.count > $0.characters.count}) {
            return max
        } else {return ""}
    }
}

// MARK: - Mathod

func createLabelWithOptimalLabelWidth (
                    requestedHeight: CGFloat,
              constantElementsWidth: CGFloat,
    acceptableWidthForTextOfOneLine: CGFloat, //When we don't want the text to be shrinked
                               text: String,
                         attributes: [String:Any]
    ) -> UILabel {

    let label = UILabel(frame: .zero)

    label.attributedText = NSAttributedString(string: text, attributes: attributes)

    let maximalLabelWidth = label.intrinsicContentSize.width

    if maximalLabelWidth < acceptableWidthForTextOfOneLine {

        label.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: maximalLabelWidth, height: requestedHeight))
        return label // We can go with this width
    }

    // Minimal width, calculated based on the longest word

    let maxWordsCharacterCount = label.text!.longestWord.characters.count
    let allLongWords: [String] = label.text!.wordList.filter {$0.characters.count == maxWordsCharacterCount}
    var sizes: [CGFloat] = []
    allLongWords.forEach {sizes.append($0.size(attributes: attributes).width)}
    let minimalWidth = (sizes.max()! + constantElementsWidth)


    // Height calculation
    var flexibleWidth = maximalLabelWidth
    var flexibleHeight = CGFloat()

    var optimalWidth = CGFloat()
    var optimalHeight = CGFloat()

    while (flexibleHeight <= requestedHeight && flexibleWidth >= minimalWidth) {

        optimalWidth = flexibleWidth
        optimalHeight = flexibleHeight

        flexibleWidth -= 1

        flexibleHeight = label.attributedText!.boundingRect(
        with: CGSize(width: flexibleWidth, height: CGFloat.greatestFiniteMagnitude),
        options: [NSStringDrawingOptions.usesLineFragmentOrigin],
        context: nil).size.height

        print("Width: \(flexibleWidth)")
        print("Height: \(flexibleHeight)")
        print("_______________________")
    }

    print("Final Width: \(optimalWidth)")
    print("Final Height: \(optimalHeight)")

    label.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: optimalWidth+constantElementsWidth, height: requestedHeight))

    return label
}

// MARK: - Inputs

let text: String? = "Determine the fair price"//nil//"Select the appropriate payment method"//"Finalize the order" //"Sell the car"//"Check the payment method"
let font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.callout)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byWordWrapping
paragraphStyle.allowsDefaultTighteningForTruncation = true


let attributes: [String:Any] = [
    NSFontAttributeName: font,
    NSParagraphStyleAttributeName: paragraphStyle,
    NSBaselineOffsetAttributeName: 0
]

if text != nil {
    let label = createLabelWithOptimalLabelWidth(requestedHeight: 70, constantElementsWidth: 0, acceptableWidthForTextOfOneLine: 120, text: text!, attributes: attributes)

    label.frame.width
    label.frame.height

    label.backgroundColor = .white
    label.lineBreakMode = .byWordWrapping
    label.numberOfLines = 3

    hostView.addSubview(label)
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM