子视图的边界在 layoutSubviews() 中为零

Subview's bounds are zero in layoutSubviews()

自定义 UIView 的子视图的子视图的边界在 layoutSubviews() 中似乎为 0,因此在 layoutSubviews() 中使用边界是一个问题。

为了说明问题,我在 GitHub 上放了一个演示:SOBoundsAreZero

这里是自定义视图实现的直接 link:DemoView.swift

自定义视图"DemoView"的结构是这样的:

DemoView
    firstLevelSubview
        secondLevelSubview

正在使用自动布局以编程方式创建此结构。

调用layoutSubviews()时,视图和firstLevelSubview有预期的边界,但secondLevelSubview的边界为0。

我希望所有使用自动布局的子视图都有正确的边界,至少在最后一次调用 layoutSubviews 时是这样。

该结构是对真实案例的抽象。为了避免这个问题,可以将 secondLevelSubview 作为第一级子视图添加到 DemoView。虽然,这是个东西,但在实际情况下是不可行的。

我觉得我在这里遗漏了一些简单的东西,即使这是预期的行为。

首先,您的视图正在被毫无问题地解释,因为您有一个基于“CGRect”的初始化器,所以它得到了它的维度! 第二, 您的第一个子视图在其父视图获得其尺寸后被初始化。第一个子视图尺寸取决于准备好的父视图尺寸。所以这里没问题,就像你提到的那样。 但是与此同时,您的第一个子视图正在尝试获取其尺寸,您的第二个子视图也开始根据尚未准备好的第一个子视图获取其尺寸,因此它一无所获。 我建议为每个子视图制作不同的功能。 在您的视图控制器中使用其框架初始值设定项从视图 class 创建您的对象。在‘viewDidLoad’中调用第一个子视图函数,在‘viewDidLoadSubView’中调用第二个子视图函数。 希望能帮助到你。

我在 layoutSubviews() 中调用 secondLevelSubview.layoutIfNeeded() 解决了这个问题。

override func layoutSubviews() {
    super.layoutSubviews()

    secondLevelSubview.layoutIfNeeded()

    firstLevelSubview.layer.cornerRadius = bounds.width / 2
    secondLevelSubview.layer.cornerRadius = secondLevelSubview.bounds.width / 2

    print("bounds.width: \(bounds.width), contentView.bounds.width: \(firstLevelSubview.bounds.width), backgroundView.bounds.width: \(secondLevelSubview.bounds.width)")
}

layoutIfNeeded()的描述是:

Use this method to force the view to update its layout immediately. When using Auto Layout, the layout engine updates the position of views as needed to satisfy changes in constraints. Using the view that receives the message as the root view, this method lays out the view subtree starting at the root. If no layout updates are pending, this method exits without modifying the layout or calling any layout-related callbacks.

所以,基本上您在这里遇到了订购问题。子视图计划布局,Auto Layout 会到达它,但它还没有完成。通过调用 layoutIfNeeded(),您告诉 Auto Layout 立即执行挂起的布局,以便您可以获得更新的帧信息。

注意: 您也可以只调用 self.layoutIfNeeded(),这将布局 DemoView 及其所有子视图。如果您有很多这样的子视图并且不想在每个子视图上调用 layoutIfNeeded(),这将很有用。

如果您尝试访问主队列下的UIView.bounds,您可以得到想要的结果。

override func layoutSubviews() {
    super.layoutSubviews()

    DispatchQueue.main.async {
        self.firstLevelSubview.layer.cornerRadius = self.bounds.width / 2
        self.secondLevelSubview.layer.cornerRadius = self.secondLevelSubview.bounds.width / 2
        print("bounds.width: \(self.bounds.width), contentView.bounds.width: \(self.firstLevelSubview.bounds.width), backgroundView.bounds.width: \(self.secondLevelSubview.bounds.width)")
    }
}

//print:
//bounds.width: 64.0, contentView.bounds.width: 64.0, backgroundView.bounds.width: 54.0

但是为什么呢?所以在这里我深入研究你的问题。这是您的 DemoView layoutSubviews 方法

的调用视图层次结构
DemoView
|
|___layoutSubviews
    |
    |___firstLevelSubview
        |
        |___layoutSubviews //Here you are trying to access bounds of second view which is still need to be resize or achieve it's bound
            |
            |___ secondLevelSubview
                | 
                |___layoutSubviews

因此在这种情况下,主队列将帮助您获得任何 UIView.subView.

的实际边界

另一种情况我做到了,我尝试使用如下 firstLevelSubview 约束将 secondLevelSubview 添加到 DemoView 本身:

fileprivate func setupViews() {
    backgroundColor = UIColor.clear

    firstLevelSubview.translatesAutoresizingMaskIntoConstraints = false
    firstLevelSubview.layer.masksToBounds = true
    firstLevelSubview.backgroundColor = UIColor.green
    addSubview(firstLevelSubview)
    firstLevelSubview.topAnchor.constraint(equalTo: topAnchor).isActive = true
    firstLevelSubview.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
    firstLevelSubview.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
    firstLevelSubview.leftAnchor.constraint(equalTo: leftAnchor).isActive = true

    secondLevelSubview.translatesAutoresizingMaskIntoConstraints = false
    secondLevelSubview.layer.masksToBounds = true
    secondLevelSubview.backgroundColor = UIColor.magenta
//  firstLevelSubview.addSubview(secondLevelSubview)
    addSubview(secondLevelSubview) //Here
    secondLevelSubview.widthAnchor.constraint(equalTo: firstLevelSubview.widthAnchor, multiplier: 0.84).isActive = true
    secondLevelSubview.heightAnchor.constraint(equalTo: firstLevelSubview.heightAnchor, multiplier: 0.84).isActive = true
    secondLevelSubview.centerXAnchor.constraint(equalTo: firstLevelSubview.centerXAnchor).isActive = true
    secondLevelSubview.centerYAnchor.constraint(equalTo: firstLevelSubview.centerYAnchor).isActive = true
}

它遵循的层次结构如下:

DemoView
    |
    |___layoutSubviews
        |
        |___firstLevelSubview
            |
            |___layoutSubviews
            |
            |___secondLevelSubview
            |
            |___layoutSubviews

因此,在上述情况下,如果您尝试在没有任何主队列的情况下打印 UIView.bounds,您将获得所需的结果。

告诉我,这有助于您理解层次结构。