如果为目标视图指定了初始框架,为什么 AutoLayout 不遵守约束?

Why would AutoLayout not respect constraints if an initial frame is specified for the target view?

我们有一个奇怪的情况,我们的视图没有按预期显示,我们已经将其追溯到 AutoLayout 和一些我们看不出为什么它会改变事情的事件序列。

为简化示例,这里有一些测试代码,您可以直接在 Xcode 游乐场中 运行。 (如果重要的话,我们使用 Xcode 11.3.1。)

首先,我们定义一个InlineConfigure运算符,像这样...

// 'InlineConfigure' operator
infix operator ~> : AssignmentPrecedence

@discardableResult
public func ~> <T>(item:T, _ configure:(T)->Void) -> T {
    configure(item)
    return item
}

接下来,为了说明这不是在不期望的情况下释放某些东西的情况,我们创建了一个 UIView 子类专门用于记录...

class SomeView : UIView {

    override init(frame: CGRect) {
        super.init(frame:frame)
        print("SomeView created")
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit {
        print("SomeView deinitialized")
    }
}

以上两点都准备就绪后,请看一下这段代码。它按预期工作。

class TestViewController : UIViewController {

    let margin:CGFloat = 24

    override func loadView() {

        view = UIView()
        view.backgroundColor = .yellow

        SomeView()~>{

            [=12=].backgroundColor = .blue
            [=12=].translatesAutoresizingMaskIntoConstraints = false
            view.addSubview([=12=])

            NSLayoutConstraint.activate([
                [=12=].leftAnchor.constraint(equalTo: view.leftAnchor, constant: margin),
                [=12=].topAnchor.constraint(equalTo: view.topAnchor, constant: margin),
                [=12=].rightAnchor.constraint(equalTo: view.rightAnchor, constant: -margin),
                [=12=].bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -margin)
            ])
        }
    }
}

PlaygroundPage.current.liveView = TestViewController()

但是,如果我们更改创建 SomeView 的行以采用具有非零区域(例如,宽度和高度)的框架,那么相同的代码将不再有效,您也不会完全看不到蓝景。

override func loadView() {

    view = UIView()
    view.backgroundColor = .yellow

    SomeView(frame:CGRect(x: 0, y: 0, width: 20, height: 20))~>{

        [=13=].backgroundColor = .blue
        [=13=].translatesAutoresizingMaskIntoConstraints = false
        view.addSubview([=13=])

        NSLayoutConstraint.activate([
            [=13=].leftAnchor.constraint(equalTo: view.leftAnchor, constant: margin),
            [=13=].topAnchor.constraint(equalTo: view.topAnchor, constant: margin),
            [=13=].rightAnchor.constraint(equalTo: view.rightAnchor, constant: -margin),
            [=13=].bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -margin)
        ])
    }
}

将框架设置为零区域,它再次起作用...

SomeView(frame:CGRect(x: 0, y: 0, width: 0, height: 20))~>{

    [...]

}

但是回到非零框架,如果将约束设置代码移到配置闭包之外,那么无论框架是否有区域,它总是有效的!

override func loadView() {

    view = UIView()
    view.backgroundColor = .yellow

    let someView = SomeView(frame:CGRect(x: 0, y: 0, width: 20, height: 20))~>{

        [=15=].backgroundColor = .blue
        [=15=].translatesAutoresizingMaskIntoConstraints = false
        view.addSubview([=15=])
    }

    NSLayoutConstraint.activate([
        someView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: margin),
        someView.topAnchor.constraint(equalTo: view.topAnchor, constant: margin),
        someView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -margin),
        someView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -margin)
    ])
}

让我们感到困惑的是在前面的代码中,[=19=] 闭包内部是对新创建视图的强引用(当它作为子视图添加时,父级也持有对它的引用所以它没有卸载),并且在外部将完全相同的引用分配给 someView 然后它执行与闭包中的代码完全相同的代码。除了赋值外,闭包和外部代码之间没有任何东西,但它给出了不同的结果!但是为什么?

谁能解释为什么我们会看到这种行为?我们缺少什么?

我会说这是因为游乐场是魔鬼的杰作。我 运行 你的代码在 真实 iOS 应用程序项目中,即使使用

它也能按预期工作
SomeView(frame:CGRect(x: 0, y: 0, width: 20, height: 20))

基本上我不希望 playground 正确地经历视图控制器的所有生命周期阶段。