使用 autolayout 和 authoshrink 为水平堆栈视图中的两个标签设置相同的字体大小

Set the same font size for two labels within a horizontal stack view with autolayout and authoshrink

提问人:wasi 提问时间:9/1/2023 最后编辑:wasi 更新时间:9/2/2023 访问量:71

问:

我需要创建一个视图,在该视图中,我希望两个标签并排放置,间距相等。问题是这些标签将具有不同的文本,具体取决于后端响应。我想平等地调整两个标签的字体大小,因为现在当两个标签中的一个具有非常大的文本时,自动布局只会调整该标签。此外,这种方法将帮助我使用像iPhone SE这样的小屏幕,因为同样的事情也会发生。我怎样才能做到这一点?

Problem

我确实尝试设置相等的宽度、高度和纵横比约束,但我无法实现我想要的。此外,两个标签在左上角保持对齐也非常重要(我通过将水平堆栈对齐设置为顶部来实现了这一点)。

我尝试过的一件事是:

One of the things that I have tried

我也尝试了这里提出的解决方案,但它对我不起作用。

更多信息和真实示例:真实示例 好的,所以预期的布局如下:

1-我有一个垂直堆栈视图,带有一个标签和水平堆栈视图

2- 水平堆栈视图具有以下插图: 水平堆栈视图插图

3- 水平堆栈视图的分布 = 等间距

4-两个标签启用了自动收缩功能,最小字体大小为9pts

问题是,如果我想在两个标签之一中添加一个很长的项目名称,我需要另一个标签具有相同的 fon 大小,并且两个标签需要保持水平堆栈视图边距并保持项目在同一行中。例如: 一方面,“非常非常非常长的项目名称”应保持在同一行中,并调整两个标签的字体大小以执行此操作,另一方面,“LongItemNameWithoutSpaces”也应执行相同的操作。长名称示例

iOS 字体 UIKit 标签 自动布局

评论

0赞 DonMag 9/1/2023
您是否需要标签的宽度相等?
0赞 wasi 9/1/2023
并非如此,我只需要两个标签保持相同的字体大小,并在水平堆栈视图中以相等的间距居中
0赞 DonMag 9/1/2023
如果您只有两个标签,则不清楚“相等间距”是什么意思?你能做几个屏幕截图,使用几个不同的“真实世界”文本字符串来准确显示你想要它的外观吗?
0赞 wasi 9/1/2023
@DonMag我添加了更多信息。如果你能帮助我,我将不胜感激。提前致谢:)
0赞 DonMag 9/1/2023
好的 - 还不是很清楚......是否希望每标签都与字体大小匹配?或者,您是否希望所有标签都匹配?所以,这个的左或右版本:i.stack.imgur.com/Ysbya.png......还是别的什么?

答:

0赞 DonMag 9/2/2023 #1

没有用于“匹配”标签自动缩小字体大小的 API。

为此,您需要:

  • 遍历列表中的字符串
  • 找到适合最长文本的最小字体大小
  • 再次遍历标签,将该字体设置为所有标签

我们可以按照这些思路使用代码......

假设我们有这些属性:

let defaultFont: UIFont = .systemFont(ofSize: 16.0)
let maxFontSize: CGFloat = 16.0
let minFontSize: CGFloat = 9.0
let rowSpacing: CGFloat = 4.0
let columnSpacing: CGFloat = 20.0

// array of strings
let items: [String] = [
    "Carrots", "Tomatos",
    "Bananas", "Cola",
    "Salad Dressing", "Lettuce",
    "Milk", "Eggo Frozen Waffles",
]

让我们这样做:

    // create a label to use for sizing
    let sizingLabel = UILabel()
    var sz: CGSize = .zero
    
    // max label width is one-half of main stack width
    //  minus column spacing (the center "gap")
    let maxW: CGFloat = floor((mainStack.bounds.width - columnSpacing) * 0.5)
    
    var curFontSize: CGFloat = maxFontSize
    var curFont: UIFont = .init(descriptor: defaultFont.fontDescriptor, size: curFontSize)
    
    // loop through all item labels...
    //  if the label is too wide, reduce the font size
    //  until the label fits, or we reach minimum font size
    sizingLabel.font = curFont
    for i in 0..<items.count {
        sizingLabel.text = items[i]
        sz = sizingLabel.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        while ceil(sz.width) > maxW && curFontSize > minFontSize {
            curFontSize -= 0.5
            curFont = .init(descriptor: defaultFont.fontDescriptor, size: curFontSize)
            sizingLabel.font = curFont
            sz = sizingLabel.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        }
    }

    // loop through the labels, setting font for all labels to the same calculated font size
    // eachLabel.font = curFont

因此,获得所需布局的一种方法是使用垂直轴堆栈视图,每个“标签对”或“行”都是水平轴堆栈视图。

我们可以得到这个:

enter image description here

像这样 - 标签是黄色的,垂直堆栈视图背景是青色的,自定义视图背景是非常浅的灰色:

enter image description here

如果我们使用 最大字体大小 和 最小字体大小 ,所有这些标签都适合16916

enter image description here

如果我们添加一个稍微过长的标签,我们可以找到最大的字体大小 - 小于 16 - 并将所有标签设置为该字体大小:

enter image description here

再添加几个“短”项目,我们仍然使用适合“鸡蛋冷冻华夫饼”的字体大小:

enter image description here

现在我们添加“汤米的生日蛋糕”,我们变得更小了一点:

enter image description here

当我们添加“Vanilla Birthday Cake for Tommy”时,我们点击了最小字体大小:9

enter image description here

依此类推:

enter image description here

enter image description here

强烈建议您尝试一下,使用上面的代码片段,以便您真正了解您需要做什么。

这里有一些完整的示例代码,你可以玩...


自定义 UIView 子类:

class MyListView: UIView {
    
    public var defaultFont: UIFont = .systemFont(ofSize: 16.0) { didSet { setNeedsLayout() } }
    public var maxFontSize: CGFloat = 16.0 { didSet { setNeedsLayout() } }
    public var minFontSize: CGFloat = 9.0 { didSet { setNeedsLayout() } }
    public var rowSpacing: CGFloat = 4.0 { didSet { setNeedsLayout() } }
    
    // space between "columns" of item labels
    public var columnSpacing: CGFloat = 20.0 { didSet { setNeedsLayout() } }
    
    // "padding" around main stack view
    public var insets: UIEdgeInsets = .init(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0) {
        didSet { updateMainStack() }
    }
    
    // during development, toggle background colors so we can see the framing
    public var devColors: Bool = false { didSet { setNeedsLayout() } }
    
    public var items: [String] = [] {
        didSet {
            let numRows: Int = Int(ceil(Double(items.count) / 2.0))
            // remove any extra rows
            while mainStack.arrangedSubviews.count > numRows {
                mainStack.arrangedSubviews.last?.removeFromSuperview()
            }
            // add any new rows
            while mainStack.arrangedSubviews.count < numRows {
                let rowStack = UIStackView()
                rowStack.axis = .horizontal
                rowStack.alignment = .top
                rowStack.spacing = columnSpacing
                rowStack.distribution = .fillEqually
                for _ in 0..<2 {
                    let label = UILabel()
                    rowStack.addArrangedSubview(label)
                }
                mainStack.addArrangedSubview(rowStack)
            }
            // clear current label refs
            labelRefs = []
            // get refs to the labels for convenience
            mainStack.arrangedSubviews.forEach { v in
                // we know what we're doing, but we want to safely unwrap anyway
                if let sv = v as? UIStackView, sv.arrangedSubviews.count == 2 {
                    if let vl = sv.arrangedSubviews[0] as? UILabel {
                        labelRefs.append(vl)
                    }
                    if let vl = sv.arrangedSubviews[1] as? UILabel {
                        labelRefs.append(vl)
                    }
                }
            }
            for (i, label) in labelRefs.enumerated() {
                // if we have an odd number of items, the
                //  last item-label will use " "
                label.text = i < items.count ? items[i] : " "
            }
            setNeedsLayout()
        }
    }
    
    // for convenience, so we can easily loop through all the item labels
    private var labelRefs: [UILabel] = []
    
    private let mainStack: UIStackView = {
        let v = UIStackView()
        v.axis = .vertical
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        updateMainStack()
    }
    
    // if the insets are changed, we need to "reset" the constraints
    private func updateMainStack() {
        mainStack.removeFromSuperview()
        addSubview(mainStack)
        NSLayoutConstraint.activate([
            mainStack.topAnchor.constraint(equalTo: topAnchor, constant: insets.top),
            mainStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: insets.left),
            mainStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -insets.right),
            mainStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -insets.bottom),
        ])
        mainStack.spacing = rowSpacing
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // column spacing may have changed
        mainStack.arrangedSubviews.forEach { v in
            if let sv = v as? UIStackView {
                sv.spacing = columnSpacing
            }
        }
        
        // create a label to use for sizing
        let sizingLabel = UILabel()
        var sz: CGSize = .zero
        
        // max label width is one-half of main stack width
        //  minus column spacing (the center "gap")
        let maxW: CGFloat = floor((mainStack.bounds.width - columnSpacing) * 0.5)
        
        var curFontSize: CGFloat = maxFontSize
        var curFont: UIFont = .init(descriptor: defaultFont.fontDescriptor, size: curFontSize)
        
        // loop through all item labels...
        //  if the label is too wide, reduce the font size
        //  until the label fits, or we reach minimum font size
        sizingLabel.font = curFont
        for i in 0..<items.count {
            sizingLabel.text = items[i]
            sz = sizingLabel.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
            while ceil(sz.width) > maxW && curFontSize > minFontSize {
                curFontSize -= 0.5
                curFont = .init(descriptor: defaultFont.fontDescriptor, size: curFontSize)
                sizingLabel.font = curFont
                sz = sizingLabel.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
            }
        }
        
        labelRefs.forEach { v in
            // set font for all labels to the same calculated font size
            v.font = curFont

            // during development
            v.backgroundColor = devColors ? .yellow : .clear
        }

        // during development
        mainStack.backgroundColor = devColors ? .cyan : .clear

        // we probably want to adjust "row spacing" based on resulting font size
        mainStack.spacing = rowSpacing * (curFontSize / maxFontSize)
    }
    
}

使用该视图的视图控制器:

class FontMatchVC: UIViewController {
    
    let samples: [String] = [
        "Carrots", "Tomatos",
        "Bananas", "Cola",
        "Salad Dressing", "Lettuce",
        "Milk", "Eggo Frozen Waffles",
        "Soup", "Chicken",
        "Birthday Cake for Tommy", "Life Cereal",
        "Eggs", "Vanilla Birthday Cake for Tommy",
        "Chocalate Birthday Cake for Tommy", "Oatmeal",
        "Hot Dogs", "Hamburgers",
        "Ketchup",
    ]

    var itemCount: Int = 0
    
    let myListView = MyListView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // controls to add items and toggle dev-mode colors
        let controlsStack = UIStackView()
        controlsStack.axis = .vertical
        controlsStack.spacing = 8.0
        
        let tmpLabel = UILabel()
        tmpLabel.text = "Show Dev Mode Colors"
        let tmpSwitch = UISwitch()
        tmpSwitch.addTarget(self, action: #selector(toggleColors(_:)), for: .valueChanged)
        let tmpStack = UIStackView(arrangedSubviews: [tmpLabel, tmpSwitch])
        controlsStack.addArrangedSubview(tmpStack)
        
        let btn = UIButton()
        btn.backgroundColor = .systemRed
        btn.layer.cornerRadius = 8.0
        btn.setTitle("Add Item", for: [])
        btn.addTarget(self, action: #selector(addItem(_:)), for: .touchUpInside)
        controlsStack.addArrangedSubview(btn)
        
        let v = UIView()
        v.backgroundColor = UIColor(red: 0.5, green: 0.75, blue: 1.0, alpha: 1.0)
        v.heightAnchor.constraint(equalToConstant: 2.0).isActive = true
        controlsStack.addArrangedSubview(v)
        
        let titleLabel = UILabel()
        titleLabel.font = .systemFont(ofSize: 20.0, weight: .bold)
        titleLabel.textAlignment = .center
        titleLabel.text = "Grocery List"
        titleLabel.backgroundColor = .systemBlue
        titleLabel.textColor = .white
        
        controlsStack.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(controlsStack)
        
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(titleLabel)
        
        myListView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(myListView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            controlsStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            controlsStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            controlsStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            
            titleLabel.topAnchor.constraint(equalTo: controlsStack.bottomAnchor, constant: 20.0),
            titleLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            titleLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            
            myListView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8.0),
            myListView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            myListView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            
        ])
        
        addItem(nil)
    }
    
    @objc func addItem(_ sender: UIButton?) {
        let n: Int = itemCount % samples.count
        myListView.items = Array(samples[0...n])
        itemCount += 1
    }

    @objc func toggleColors(_ sender: UISwitch) {
        myListView.backgroundColor = sender.isOn ? UIColor(white: 0.95, alpha: 1.0) : .clear
        myListView.devColors = sender.isOn
    }
    
}

运行时看起来像这样:

enter image description here

“添加项目”按钮更改列表中的项目,以及切换“开发模式”颜色的开关,以便我们可以轻松查看框架。