ios - 在渲染周期中调用自动布局方法的序列

标签 ios swift autolayout ios-autolayout

我在阅读有关自动布局渲染管道的内容,我的意思是自动布局如何在幕后工作。有一些方法会在 autoLayout 渲染的不同阶段被调用,比如

  • layoutIfNeeded()
  • layoutSubviews()
  • updateConstraints()
  • updateConstraintsIfNeeded()

但我不知道何时调用哪个方法以及该方法的意义是什么,如果我想使用自动布局,那么我可以按什么顺序使用这些方法以及如何控制自动布局渲染管道

最佳答案

通常你不需要关心自动布局方法链。您只需要为 View 创建约束以定义它们的大小和位置。您可以在 View 的生命周期中随时添加/删除、激活/停用约束,但您希望始终拥有一组可满足(不冲突)但又完整的约束集。

举个例子。您可以告诉自动布局按钮 A 应该是 50 点宽,20 点高,它的左上角位于 viewController 中的点 (0,0)看法。现在,这是对按钮 A 的非冲突但完整的约束集。但是假设您想在用户点击该按钮时展开该按钮。因此,在点击处理程序中,您将添加一个新约束,说明按钮应为 100 点宽 - 现在您有不可满足的约束 - 有一个约束表示它应该为 50 点宽,另一个约束表示它应该为 100 点宽。因此,为防止冲突,在激活新约束之前,您必须停用旧约束。不完全约束是一种相反的情况,假设您停用了旧的宽度约束,但从未激活新的约束。然后自动布局可以计算位置(因为有定义它的约束)和高度,但不是宽度,这通常以未定义的行为结束(现在在 UIButton 的情况下是不正确的,因为它具有固有大小,它隐含地定义了它的宽度和高度,但我希望你明白这一点)。

因此,何时创建这些约束取决于您(在我的示例中,您是在用户点击按钮时操纵它们)。通常,如果是 UIView 子类,则从初始化程序开始,或者从 UIViewController 子类的 loadView 开始,您可以在其中定义和激活默认约束集.然后您可以使用处理程序对用户事件使用react。我的建议是在 loadView 中准备所有约束,将它们保存在属性中,并在必要时激活/停用它们。

但是当然有一些限制,例如何时以及如何不创建新约束 - 有关这些情况的更详细讨论,我真的建议查看 Advanced Autolayout Toolbox通过 objc.io。

编辑

请参阅以下简单自定义 SongView 示例,它使用自动布局进行布局,还支持通过激活/停用约束来动态更改约束。您可以简单地将整个代码复制粘贴到 playground 中并在那里进行测试,或者将其包含在项目中。

请注意,我没有调用任何自动布局生命周期方法,setNeedsLayoutlayoutIfNeeded 除外。 setNeedsLayout 设置一个标志,告诉自动布局约束已更改,然后 layoutIfNeeded 告诉它重新计算帧。通常,这会自动发生,但要为约束变化设置动画,我们需要明确地告诉它 - 请参阅 SongView 中的 setExpanded 方法。有关在动画中使用自动布局的更多详细说明,请参阅 my different answer .

import UIKit
import PlaygroundSupport

class ViewController: UIViewController {

    let songView = SongView()
    let button = UIButton()

    override func loadView() {
        super.loadView()
        view.backgroundColor = .white
        self.view.addSubview(button)
        self.view.addSubview(songView)
        button.setTitle("Expand/Collapse", for: .normal)
        button.setTitleColor(.blue, for: .normal)
        button.addTarget(self, action: #selector(expandCollapse), for: .touchUpInside)

        button.translatesAutoresizingMaskIntoConstraints = false
        songView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            // button has intrinsic size, no need to define constraints for size, position is enough
            button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -50),
            button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),

            // songView has defined its height (see SongView class), but not width, therefore we need more constraints
            songView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
            songView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
            songView.rightAnchor.constraint(equalTo: self.view.rightAnchor),
            ])
    }

    @objc func expandCollapse() {
        if songView.isExpanded {
            songView.setExpanded(to: false, animated: true)
        } else {
            songView.setExpanded(to: true, animated: true)
        }
    }
}

class SongView: UIView {

    private let numberLabel: UILabel = UILabel()
    private let nameLabel: UILabel = UILabel()

    private var expandedConstraints: [NSLayoutConstraint] = []
    private var collapsedConstraints: [NSLayoutConstraint] = []

    // this can be triggered by some event
    private(set) var isExpanded: Bool = false

    func setExpanded(to expanded: Bool, animated: Bool) {
        self.isExpanded = expanded
        if animated {
            if expanded {
                // setup expanded state
                NSLayoutConstraint.deactivate(collapsedConstraints)
                NSLayoutConstraint.activate(expandedConstraints)
            } else {
                // setup collapsed
                NSLayoutConstraint.deactivate(expandedConstraints)
                NSLayoutConstraint.activate(collapsedConstraints)
            }
            self.setNeedsLayout()
            UIView.animate(withDuration: 0.2, animations: {
                self.layoutIfNeeded()
            })
        } else {
            // non animated version (no need to explicitly call setNeedsLayout nor layoutIfNeeded)
            if expanded {
                // setup expanded state
                NSLayoutConstraint.deactivate(collapsedConstraints)
                NSLayoutConstraint.activate(expandedConstraints)
            } else {
                // setup collapsed
                NSLayoutConstraint.deactivate(expandedConstraints)
                NSLayoutConstraint.activate(collapsedConstraints)
            }
        }
    }

    var data: (String, String)? {
        didSet {
            numberLabel.text = data?.0
            nameLabel.text = data?.1
        }
    }

    init() {
        super.init(frame: CGRect.zero)

        setupInitialHierarchy()
        setupInitialAttributes()
        setupInitialLayout()
    }

    fileprivate func setupInitialHierarchy() {
        self.addSubview(numberLabel)
        self.addSubview(nameLabel)
    }

    fileprivate func setupInitialAttributes() {
        numberLabel.font = UIFont.boldSystemFont(ofSize: UIFont.preferredFont(forTextStyle: UIFontTextStyle.body).pointSize)
        numberLabel.textColor = UIColor.darkGray
        numberLabel.text = "0"
        numberLabel.textAlignment = .right

        nameLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
        nameLabel.text = "NONE"
        nameLabel.textAlignment = .left

        self.backgroundColor = UIColor.lightGray
    }

    fileprivate func setupInitialLayout() {
        self.translatesAutoresizingMaskIntoConstraints = false
        numberLabel.translatesAutoresizingMaskIntoConstraints = false
        nameLabel.translatesAutoresizingMaskIntoConstraints = false

        // just randomly selected different layouts for collapsed and expanded states
        expandedConstraints = [
            numberLabel.widthAnchor.constraint(equalToConstant: 35),
            self.heightAnchor.constraint(equalToConstant: 80),
        ]
        collapsedConstraints = [
            numberLabel.widthAnchor.constraint(equalToConstant: 50),
            self.heightAnchor.constraint(equalToConstant: 40),
        ]

        // activating collapsed as default layout
        NSLayoutConstraint.activate(collapsedConstraints)
        NSLayoutConstraint.activate([
            numberLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 4),
            numberLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -4),
            numberLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 4),

            nameLabel.centerYAnchor.constraint(equalTo: numberLabel.centerYAnchor),
            nameLabel.leftAnchor.constraint(equalTo: numberLabel.rightAnchor, constant: 8),
            nameLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -4)
            ])
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

PlaygroundPage.current.liveView = ViewController()

关于ios - 在渲染周期中调用自动布局方法的序列,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48221300/

相关文章:

ios - 'CALayer 位置包含 NaN : [nan nan]' on UIScrollView

ios - 在代码中查找在 IB 中创建的布局约束

ios - BoundingRectWithSize - 选择使用的字体和字体大小

ios - 对齐指南()不覆盖 subview

ios - 从单个 UIPicker 获取两个标签的值

ios - 在 UITableViewCell 中重新加载 Collectionview Cell

ios - 自动布局:指定 View 和导航栏之间的间距

cocoa-touch - 如何调用电话、发送短信或在 map 上查找地址?

iphone - 检查对象是否为 UIKeyboard

ios - 击键时返回 Action