如何在不重载 CPU 的情况下动态更新 UILabel 宽度

How to update a UILabel width dynamically without overloading the CPU

iOS中的标签是像(1)一样创建的,没有水平边距,一点也不美观。 我想创建一个像(2)中的标签,弯曲的边缘和左右边距

这个标签的内容每秒更新2次,它的宽度必须动态变化。

所以我创造了这个class

@IBDesignable
class BeautifulLabel : UILabel {
  
//  private var internalRect : CGRect? = .zero

  override func drawText(in rect: CGRect) {
    let insets = UIEdgeInsets(top: marginTop,
                              left: marginLeft,
                              bottom: marginBottom,
                              right: marginRight)
    super.drawText(in: rect.inset(by: insets))
  }
  
  
  @IBInspectable var cornerRadius: CGFloat = 0 {
    didSet {
      self.layer.cornerRadius = cornerRadius
      self.layer.masksToBounds = cornerRadius > 0
    }
  }
  
  @IBInspectable var marginTop: CGFloat = 0
  @IBInspectable var marginBottom: CGFloat = 0
  @IBInspectable var marginLeft: CGFloat = 0
  @IBInspectable var marginRight: CGFloat = 0

  override func layoutSubviews() {
    super.layoutSubviews()
    var bounds = self.bounds
    bounds.size.width += marginLeft + marginRight
    bounds.size.height += marginTop + marginBottom
    self.bounds = bounds
  }

这行得通,但在 layoutSubviews() 中调整 self.bounds,使该方法再次被调用,导致巨大的循环,CPU 尖峰和内存泄漏。

然后我试了这个:

override var text: String? {
    didSet {
      let resizingLabel = UILabel(frame: self.bounds)
      resizingLabel.text = self.text
      
      var bounds = resizingLabel.textRect(forBounds: CGRect(x: 0, y: 0, width: 500, height: 50), limitedToNumberOfLines: 1)
      
      bounds.size.width += marginLeft + marginRight
      bounds.size.height += marginTop + marginBottom
      self.bounds = bounds
    }
  }

这根本行不通。标签没有调整到合适的大小。

标签必须只有一行、固定高度、截尾和固定字体大小(系统 17)。我对它的宽度感兴趣。

有什么想法吗?

视图不应更改其自身的大小。它应该只改变它的 intrinsicContentSize.

当您将视图添加到视图层次结构时,即指定它是否应遵守固有内容大小(例如,内容拥抱设置、抗压缩性、没有明确的宽度和高度限制等)。如果您这样做,自动布局引擎将为您完成所有工作。

因此,举个例子,极简主义的方法就是覆盖 intrinsicContentSize:

@IBDesignable
class BeautifulLabel: UILabel {
    @IBInspectable var marginX: CGFloat = 0       { didSet { invalidateIntrinsicContentSize() } }
    @IBInspectable var marginY: CGFloat = 0       { didSet { invalidateIntrinsicContentSize() } }
    @IBInspectable var cornerRadius: CGFloat = 0  { didSet { layer.cornerRadius = cornerRadius } }

    override var intrinsicContentSize: CGSize {
        let size = super.intrinsicContentSize
        return CGSize(width: size.width + marginX * 2, height: size.height + marginY * 2)
    }
}

一个更完整的例子可能是一个 UIView 子类,其中标签是一个子视图,插入适当的边距:

@IBDesignable
class BeautifulLabel: UIView {
    @IBInspectable var marginTop: CGFloat = 0       { didSet { didUpdateInsets() } }
    @IBInspectable var marginBottom: CGFloat = 0    { didSet { didUpdateInsets() } }
    @IBInspectable var marginLeft: CGFloat = 0      { didSet { didUpdateInsets() } }
    @IBInspectable var marginRight: CGFloat = 0     { didSet { didUpdateInsets() } }

    @IBInspectable var cornerRadius: CGFloat = -1   { didSet { setNeedsLayout() } }
    
    @IBInspectable var text: String? {
        get {
            label.text
        }
        
        set {
            label.text = newValue
            invalidateIntrinsicContentSize()
        }
    }

    @IBInspectable var font: UIFont? {
        get {
            label.font
        }
        
        set {
            label.font = newValue
            invalidateIntrinsicContentSize()
        }
    }

    private var topConstraint: NSLayoutConstraint!
    private var leftConstraint: NSLayoutConstraint!
    private var rightConstraint: NSLayoutConstraint!
    private var bottomConstraint: NSLayoutConstraint!

    private let label: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    override var intrinsicContentSize: CGSize {
        let size = label.intrinsicContentSize
        return CGSize(width: size.width + marginLeft + marginRight,
                      height: size.height + marginTop + marginBottom)
    }

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        configure()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        configure()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()

        let maxCornerRadius = min(bounds.width, bounds.height) / 2

        if cornerRadius < 0 || cornerRadius > maxCornerRadius {
            layer.cornerRadius = maxCornerRadius
        } else {
            layer.cornerRadius = cornerRadius
        }
    }
}

private extension BeautifulLabel {
    func configure() {
        addSubview(label)
        
        topConstraint = label.topAnchor.constraint(equalTo: topAnchor, constant: marginTop)
        leftConstraint = label.leftAnchor.constraint(equalTo: leftAnchor, constant: marginLeft)
        rightConstraint = rightAnchor.constraint(equalTo: label.rightAnchor, constant: marginRight)
        bottomConstraint = bottomAnchor.constraint(equalTo: label.bottomAnchor, constant: marginBottom)
        
        NSLayoutConstraint.activate([leftConstraint, rightConstraint, topConstraint, bottomConstraint])
    }
    
    func didUpdateInsets() {
        topConstraint.constant = marginTop
        leftConstraint.constant = marginLeft
        rightConstraint.constant = marginRight
        bottomConstraint.constant = marginBottom
        
        invalidateIntrinsicContentSize()
    }
}

现在在这种情况下,我只公开了 textfont,但是您显然会重复您想要公开的任何其他属性。

但我们不要迷失在上述实现的细节中。最重要的是,视图不应该尝试调整自己的大小,而只是调整自己的 intrinsicContentSize。它应该在必要时执行 invalidateIntrinsicContentSize