如何确保当UIStackView的排列视图的大小发生变化时,UIStackView会通知它的所有子代?

问题描述 投票:0回答:1

我有一个 UIStackView 与2个排列的子UIViews。第二个UIView(标题视图)的contentHugging设置为------。high当它的内容发生变化时,第一个UIView(内容视图)会正确地拉伸它的大小。一切看起来都很好。

问题是,当UIStackView根据第二个视图的大小变化来布置它的视图时,第一个视图的子视图和子代都没有得到通知。

我以为 viewDidLayoutSubviews 应该是向下传播的,所以我加入了

override func viewDidLayoutSubviews() {
    let parentView = self.superview!

    // This is always .zero
    print(parentView.frame)

    super.viewDidLayoutSubviews()

    // This is ALSO always .zero
    print(parentView.frame)

}

到尽可能多的子视图,但它似乎从来没有报告任何视图变化不止一次,所以有这个理论。

由于我们无法观察到 UIView 框架值,怎样才能确保子代视图始终知道内容视图的框架大小,并能相对于它来调整自己的大小?

ios swift uikit uistackview
1个回答
0
投票

下面是一个完整的例子 layoutSubviews() 呼叫所有的 arrangedSubviews 的堆栈视图... 的所有子视图。

下面是开始的样子。

enter image description here

  • 堆栈视图有一个 "虚线轮廓"
  • 底部"(绿色)视图是我们的 "标题 "视图--一个多行标签在一个 UIView
  • "顶部"(黄色)视图是我们的 "内容 "视图--两个标签和一个 "圆形 "视图

每次我们点击按钮,标题视图中的文字都会发生变化。

enter image description here

enter image description here

事实上,"圆形视图 "仍然是圆形的,这告诉我们,它是一个圆形的。layoutSubviews() (我们更新 cornerRadius 的地方)正在被调用。如果没有的话,就会变成这样。

enter image description here

下面是这个例子的代码 (没有调用 @IBOutlet@IBAction connections).注意:当视图收到调用时,也会print()它们的类名和边界。

class StackExampleViewController: UIViewController {

    let testButton: UIButton = {
        let v = UIButton()
        v.setTitle("Tap Me", for: [])
        v.setTitleColor(.lightGray, for: .highlighted)
        v.backgroundColor = .blue
        return v
    }()

    let contentView: ContentView = {
        let v = ContentView()
        return v
    }()

    let titleView: TitleView = {
        let v = TitleView()
        return v
    }()

    let stackView: UIStackView = {
        let v = UIStackView()
        v.axis = .vertical
        v.alignment = .fill
        v.distribution = .fill
        v.spacing = 8
        return v
    }()

    let dashedView: DashedBorderView = {
        let v = DashedBorderView()
        return v
    }()

    let sampleData: [String] = [
        "This is the Title View",
        "A label can contain an arbitrary amount of text, but UILabel may shrink, wrap, or truncate the text, depending on the size of the bounding rectangle and properties you set.",
        "You can control the font, text color, alignment, highlighting, and shadowing of the text in the label.",
        "What's a UIButton?",
        "You can set the title, image, and other appearance properties of a button. In addition, you can specify a different appearance for each button state."
    ]

    var idx: Int = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        // standard auto-layout
        [testButton, contentView, titleView, stackView, dashedView].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
        }

        // add contentView and titleView to stackView
        stackView.addArrangedSubview(contentView)
        stackView.addArrangedSubview(titleView)

        // add button, dashedView and stackView
        //  (dashedView will be used to show the frame of stackView)
        view.addSubview(testButton)
        view.addSubview(dashedView)
        view.addSubview(stackView)

        // respect safe-area
        let g = view.safeAreaLayoutGuide

        NSLayoutConstraint.activate([

            // testButton 40-pts from top, centeredX
            testButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            testButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),

            // stackView 40-pts from testButton
            //  40-pts on each side
            stackView.topAnchor.constraint(equalTo: testButton.bottomAnchor, constant: 40.0),
            stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),

            // stackView height = 300
            stackView.heightAnchor.constraint(equalToConstant: 300.0),

            // constrain dashedView centered to stackView
            //  width and height 2-pts greater (so we can "outline" the stackView frame)
            dashedView.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: 2),
            dashedView.heightAnchor.constraint(equalTo: stackView.heightAnchor, constant: 2),
            dashedView.centerXAnchor.constraint(equalTo: stackView.centerXAnchor),
            dashedView.centerYAnchor.constraint(equalTo: stackView.centerYAnchor),

        ])

        // add touchUp target
        testButton.addTarget(self, action: #selector(self.didTap(_:)), for: .touchUpInside)

        // this will track the text for updating titleView's label
        idx = sampleData.count
        updateText()
    }

    func updateText() -> Void {
        // change the text in titleView's titleLabel
        //  let auto-layout handle ALL of the resizing
        titleView.titleLabel.text = sampleData[idx % sampleData.count]
        idx += 1
    }

    @objc func didTap(_ sender: Any) {
        updateText()
    }

}

class TitleView: UIView {

    // TitleView has a multi-line UILabel
    //  with 20-pts "padding" on each side
    //  and 12-pts "padding" on top and bottom

    var titleLabel: UILabel = {
        let v = UILabel()
        v.text = "Title Label"
        v.numberOfLines = 0
        return v
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    func commonInit() -> Void {

        backgroundColor = .green

        [titleLabel].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
            $0.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            $0.textAlignment = .center
            addSubview($0)
        }

        NSLayoutConstraint.activate([

            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
            titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
            titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
            titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),

        ])

        // we want this label's text to control its height
        titleLabel.setContentHuggingPriority(.required, for: .vertical)

    }

    override func layoutSubviews() {
        super.layoutSubviews()
        print(NSStringFromClass(type(of: self)), #function, bounds)
    }

}

class ContentView: UIView {

    // ContentView has two labels and a "round" view

    var labelA: UILabel = {
        let v = UILabel()
        v.text = "Label A"
        return v
    }()

    var labelB: UILabel = {
        let v = UILabel()
        v.text = "The Content View is Yellow"
        return v
    }()

    var roundView: RoundView = {
        let v = RoundView()
        v.backgroundColor = .orange
        return v
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    func commonInit() -> Void {

        backgroundColor = .yellow

        [labelA, labelB].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
            $0.backgroundColor = .cyan
            $0.textAlignment = .center
            addSubview($0)
        }

        roundView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(roundView)

        NSLayoutConstraint.activate([

            // constrain labelA 20-pts from top / leading
            labelA.topAnchor.constraint(equalTo: topAnchor, constant: 20.0),
            labelA.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),

            // constrain roundView 20-pts from trailing
            roundView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),

            // constrain labelA trailing 20-pts from roundView leading
            labelA.trailingAnchor.constraint(equalTo: roundView.leadingAnchor, constant: -20.0),

            // constrain roundView height equal to lableA height
            roundView.heightAnchor.constraint(equalTo: labelA.heightAnchor),
            // keep roundView square (1:1 ratio)
            roundView.widthAnchor.constraint(equalTo: roundView.heightAnchor),

            // center roundView vertically to labelA
            roundView.centerYAnchor.constraint(equalTo: labelA.centerYAnchor),

            // labelB top is 8-pts below labelA
            labelB.topAnchor.constraint(equalTo: labelA.bottomAnchor, constant: 8.0),

            // constrain labelB 20-pts from leading / trailing / bottom
            labelB.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
            labelB.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
            labelB.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20.0),

            // keep labelA and labelB heights equal
            labelA.heightAnchor.constraint(equalTo: labelB.heightAnchor),

        ])

    }

    override func layoutSubviews() {
        super.layoutSubviews()
        print(NSStringFromClass(type(of: self)), #function, bounds)
    }

}

class RoundView: UIView {

    override func layoutSubviews() {
        super.layoutSubviews()
        print(NSStringFromClass(type(of: self)), #function, bounds)
        // update cornerRadius here to keep it round
        layer.cornerRadius = bounds.size.width * 0.5
    }

}

class DashedBorderView: UIView {

    // simple view with dashed border

    var shapeLayer: CAShapeLayer!

    override class var layerClass: AnyClass {
        return CAShapeLayer.self
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

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

    func commonInit() -> Void {

        shapeLayer = self.layer as? CAShapeLayer
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = UIColor(red: 1.0, green: 0.25, blue: 0.25, alpha: 1.0).cgColor
        shapeLayer.lineWidth = 1.0
        shapeLayer.lineDashPattern = [8,8]

    }

    override func layoutSubviews() {
        super.layoutSubviews()
        print(NSStringFromClass(type(of: self)), #function, bounds)
        shapeLayer.path = UIBezierPath(rect: bounds).cgPath
    }
}

注意:当视图收到调用时,也会print()它们的类名和边界。layoutSubviews()所以你会在debug控制台看到这样的内容。

SampleApp.TitleView layoutSubviews() (0.0, 0.0, 295.0, 146.0)
SampleApp.ContentView layoutSubviews() (0.0, 0.0, 295.0, 146.0)
SampleApp.RoundView layoutSubviews() (0.0, 0.0, 49.0, 49.0)
SampleApp.TitleView layoutSubviews() (0.0, 0.0, 295.0, 105.5)
SampleApp.ContentView layoutSubviews() (0.0, 0.0, 295.0, 186.5)
SampleApp.RoundView layoutSubviews() (0.0, 0.0, 69.0, 69.5)
© www.soinside.com 2019 - 2024. All rights reserved.