【问题标题】:Subview's bounds are zero in layoutSubviews()子视图的边界在 layoutSubviews() 中为零
【发布时间】:2019-11-15 23:29:08
【问题描述】:

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

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

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

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

DemoView
    firstLevelSubview
        secondLevelSubview

这个结构是使用自动布局以编程方式创建的。

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

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

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

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

【问题讨论】:

  • 您是否添加了约束并设置 translatesAutoresizingMaskIntoConstraints = false ?
  • @Vicaren,是的,已经设置好了。为方便起见,我添加了实现的链接。

标签: ios swift uiview autolayout


【解决方案1】:

我可以通过在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()的描述是:

使用此方法强制视图立即更新其布局。 使用 Auto Layout 时,布局引擎会更新 视图以满足约束的变化。使用该视图 作为根视图接收消息,此方法布局视图 从根开始的子树。如果没有待处理的布局更新,则此 方法退出而不修改布局或调用任何 布局相关的回调。

所以,基本上你在这里有一个订购问题。子视图已安排好布局,Auto Layout 会处理它,但它还没有完成。通过调用layoutIfNeeded(),您告诉Auto Layout立即执行挂起的布局,以便您可以获取更新的框架信息。

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

【讨论】:

  • 感谢@vacawama,这对我来说最有意义。在函数中调用 layoutIfNeeded() 来设置约束,这样做之后对我来说很有意义。我认为这是我错过的“简单的事情”。
  • 有趣的是,如果你在setupViews() 中调用layoutIfNeeded()layoutSubviews() 会被调用两次,首先是0 的帧,然后是正确的帧。如果您在layoutSubviews() 中调用layoutIfNeeded(),则角半径值将仅设置一次正确值。两者最终都在同一个地方。
  • 在我也看到的真实案例中,在setupViews() 中调用layoutIfNeeded() 不会产生预期的效果。看来,它应该在layoutSubviews() 中的子视图本身上调用,正如您的回答主要指出的那样。再次感谢!
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    如果您尝试访问主队列下的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 的实际边界。

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

    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,您将获得所需的结果。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-03-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-14
      • 1970-01-01
      相关资源
      最近更新 更多