如何以从下到上增长的方式将 stackView 固定到视图

How to pin stackView to a view in such a way that it grows from bottom to top

我在 scrollView 中有一个 stackView,我想固定 stackView,使其从底部开始,随着向其添加更多视图而向上增长。

显示当前行为的图像显示 stackView 固定到视图的顶部,内容从顶部到底部。

显示所需行为的图像,显​​示堆栈视图从下到上增长。

当前代码:

CustomStackView


open class CustomStackView: UIView {
    
    public var scrollView: UIScrollView = {
        
        let view = UIScrollView(frame: .zero)
        view.isScrollEnabled = true
        view.bounces = true
        view.alwaysBounceVertical = true
        view.keyboardDismissMode = .interactive
        view.layoutMargins = .zero
        view.clipsToBounds = false
        
        return view
    }()
    
    public var stackView: UIStackView = {
        
        let stackView = UIStackView(frame: .zero)
        stackView.axis = .vertical
        stackView.alignment = .fill
        stackView.distribution = .fillProportionally
        
        let gap: CGFloat = 0
        stackView.spacing = gap
        stackView.layoutMargins = UIEdgeInsets(top: gap, left: gap, bottom: gap, right: gap)
        stackView.isLayoutMarginsRelativeArrangement = true
        
        return stackView
    }()
    
    override public init(frame: CGRect) {
        
        super.init(frame: frame)

        self.layoutMargins = .zero
        
        backgroundColor = .green

        scrollView.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(scrollView)

        NSLayoutConstraint.activate([scrollView.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor),
                                     scrollView.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor),
                                     scrollView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor),
                                     scrollView.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor)])
        
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        scrollView.addSubview(stackView)
        
        NSLayoutConstraint.activate([stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
                                     stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
                                     stackView.topAnchor.constraint(lessThanOrEqualTo: scrollView.topAnchor),
                                     stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
                                     stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)])
    }
    
    required public init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

CellView


class CellView: UIView {
    
    private let title: UILabel = {
        let label = UILabel(frame: .zero)
        label.textColor = .black
        label.font = .systemFont(ofSize: 18, weight: .medium)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.textAlignment = .left
        label.text = "Title"
        return label
    }()
    
    private let subTitle: UILabel = {
        let label = UILabel(frame: .zero)
        label.textColor = .lightGray
        label.font = .systemFont(ofSize: 14, weight: .regular)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "subTitle"
        label.isHidden = true
        return label
    }()
    
    private let seperatorView: UIView = {
        let view = UIView(frame: .zero)
        view.backgroundColor = .lightGray
        view.heightAnchor.constraint(equalToConstant: 0.5).isActive = true
        view.translatesAutoresizingMaskIntoConstraints = false
        
        return view
    }()
    
    private let headerStackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.alignment = .leading
        stackView.distribution = .fill
        stackView.spacing = 0
        stackView.translatesAutoresizingMaskIntoConstraints = false
        return stackView
    }()
    

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

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

        setupView()
    }
    
    init(title: String) {
        
        super.init(frame: .zero)
        
        setupView()
        configureView(with: title)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
   private func setupView() {
        
    self.backgroundColor = .gray
        
        headerStackView.addArrangedSubview(title)
        headerStackView.addArrangedSubview(subTitle)
        addSubview(seperatorView)
        
        addSubview(headerStackView)
        
        setupConstraints()
    }
    
   private func setupConstraints() {
        
        self.heightAnchor.constraint(equalToConstant: 50).isActive = true
    
        headerStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16).isActive = true
        headerStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
        headerStackView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
        
        seperatorView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
        seperatorView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
        seperatorView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
    }
    
    private func configureView(with title: String) {
        self.title.text = title
    }

}

ViewController:

class TestViewController: UIViewController {
    
    private let containerView: UIView = {
        let view = UIView(frame: .zero)
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = UIColor.red
        view.clipsToBounds = true
        return view
    }()
    
    private let customStackView: CustomStackView = {
        let view = CustomStackView(frame: .zero)
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupView()
        configureView()
    }
    
    func setupView() {
        view.addSubview(containerView)
        containerView.clipsToBounds = true
        NSLayoutConstraint.activate([
            containerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 40),
            containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        ])
        
        containerView.addSubview(customStackView)

        NSLayoutConstraint.activate([
            customStackView.topAnchor.constraint(equalTo: containerView.topAnchor),
            customStackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
            customStackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            customStackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
        ])
    }
    
    func configureView() {
        customStackView.stackView.addArrangedSubview(CellView(title: "Title"))
        customStackView.stackView.addArrangedSubview(CellView(title: "Tesla"))
        customStackView.stackView.addArrangedSubview(CellView(title: "Lucid"))
        customStackView.stackView.addArrangedSubview(CellView(title: "Merc"))
        customStackView.stackView.addArrangedSubview(CellView(title: "BMW"))
    }
}

我希望视图在到达顶部时以这种方式运行:

要让“CellView 堆栈”从底部增长,您需要将该堆栈视图嵌入到另一个“容器”视图中。

  • 将 stackView 添加为 stackContainer 视图的子视图
  • 约束 stackView Leading / Trailing / Bottom 都等于零
  • 将 stackView Top 大于或等于 限制为零...这将使 stackView 保持在底部,但会强制 stackContainer 在获取时增长足够高
  • 将 stackContainer 视图作为子视图添加到 scrollView
  • 将该 stackContainer 视图的所有 4 个边都约束到 scrollView 的 .contentLayoutGuide 以控制“可滚动区域”。
  • 将 stackContainer 的 width 限制为 scrollView 的 .frameLayoutGuide width

“棘手”的部分是这样的:我们还限制了 stackContainer 视图的 height 等于 scrollView 的 .frameLayoutGuide 高度,但是我们将该约束的优先级设置为低于要求——例如 .defaultHigh。这将使 stackContainer 视图等于滚动视图框架的高度,直到堆栈视图变得足够高以使其增长

这是 运行:

时的样子

Red:    your main "container" view
Green:  CustomStackView
Yellow: scrollView
Teal:   stackContainerView
Orange: stack view
Gray:   CellViews

这是经过一些修改的代码...


CustomStackView -- 看代码中注释:

open class CustomStackView: UIView {
    
    public var scrollView: UIScrollView = {
        
        let view = UIScrollView(frame: .zero)
        view.isScrollEnabled = true
        view.bounces = true
        view.alwaysBounceVertical = true
        view.keyboardDismissMode = .interactive
        view.layoutMargins = .zero
        view.clipsToBounds = false
        
        return view
    }()

    // a UIView to hold the stack view
    let stackContainerView: UIView = {
        let v = UIView()
        v.backgroundColor = .systemTeal
        return v
    }()

    // func to "auto-scroll" to the bottom of the scroll view
    public func scrollToBottom() -> Void {
        let sz = scrollView.contentSize
        let offset = sz.height - scrollView.frame.height
        UIView.animate(withDuration: 0.3, animations: {
            self.scrollView.contentOffset.y = offset
        })
    }

    public var stackView: UIStackView = {
        
        let stackView = UIStackView(frame: .zero)
        stackView.axis = .vertical
        stackView.alignment = .fill
        stackView.distribution = .fill //Proportionally
        
        let gap: CGFloat = 0
        stackView.spacing = gap
        stackView.layoutMargins = UIEdgeInsets(top: gap, left: gap, bottom: gap, right: gap)
        stackView.isLayoutMarginsRelativeArrangement = true
        
        return stackView
    }()
    
    override public init(frame: CGRect) {
        
        super.init(frame: frame)
        
        self.layoutMargins = .zero
        
        backgroundColor = .green
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(scrollView)
        
        NSLayoutConstraint.activate([scrollView.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor),
                                     scrollView.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor),
                                     scrollView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor),
                                     scrollView.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor)])

        stackContainerView.translatesAutoresizingMaskIntoConstraints = false
        stackView.translatesAutoresizingMaskIntoConstraints = false

        // add stackContainer to the scrollView
        scrollView.addSubview(stackContainerView)
        // add stackView to the stackContainer
        stackContainerView.addSubview(stackView)
        
        let contentGuide = scrollView.contentLayoutGuide
        let frameGuide = scrollView.frameLayoutGuide

        NSLayoutConstraint.activate([
            // constrain stackContainer sides to scrollView's .contentLayoutGuide
            stackContainerView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor),
            stackContainerView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor),
            stackContainerView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
            stackContainerView.bottomAnchor.constraint(equalTo: contentGuide.bottomAnchor),
            
            // constrain stackContainer width to scrollView's .frameLayoutGuide
            stackContainerView.widthAnchor.constraint(equalTo: frameGuide.widthAnchor),
            
            // constrain stackView sides to stackContainer
            stackView.leadingAnchor.constraint(equalTo: stackContainerView.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: stackContainerView.trailingAnchor),
            
            // constrain stackView to bottom of container
            stackView.bottomAnchor.constraint(equalTo: stackContainerView.bottomAnchor),

            // keep stackView Top >= stackContainer Top
            stackView.topAnchor.constraint(greaterThanOrEqualTo: stackContainerView.topAnchor),
        ])
        
        // constrain stackContainer Height to scrollView's .frameLayoutGuide Height
        let cHeight = stackContainerView.heightAnchor.constraint(equalTo: frameGuide.heightAnchor)
        // give it less-than .required Priority, so it can grow
        //  when the stackView gets taller
        cHeight.priority = .defaultHigh
        // activate this constraint
        cHeight.isActive = true
        
        // so we can see view frames for debugging during dev
        scrollView.backgroundColor = .yellow
        stackView.backgroundColor = .orange

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

TestViewController -- 唯一的变化是添加了一个点击手势识别器...每次点击都会在堆栈底部添加一个新的 CellView:

class TestViewController: UIViewController {
    
    private let containerView: UIView = {
        let view = UIView(frame: .zero)
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = UIColor.red
        view.clipsToBounds = true
        return view
    }()
    
    private let customStackView: CustomStackView = {
        let view = CustomStackView(frame: .zero)
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupView()
        configureView()
        
        // add a tap gesture recognizer
        let t = UITapGestureRecognizer(target: self, action: #selector(didTap(_:)))
        view.addGestureRecognizer(t)
    }
    
    @objc func didTap(_ g: UITapGestureRecognizer) -> Void {
        // on tap, get the current number of "cell views" in the custom stack view
        let n = customStackView.stackView.arrangedSubviews.count
        // add a new CellView
        customStackView.stackView.addArrangedSubview(CellView(title: "Cell \(n)"))
        // scroll the stack view to the bottom to show the newly added CellView
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
            self.customStackView.scrollToBottom()
        })
    }
    
    func setupView() {
        view.addSubview(containerView)
        containerView.clipsToBounds = true
        NSLayoutConstraint.activate([
            containerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 40),
            containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        ])
        
        containerView.addSubview(customStackView)
        
        NSLayoutConstraint.activate([
            customStackView.topAnchor.constraint(equalTo: containerView.topAnchor),
            customStackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
            customStackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            customStackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
        ])
    }
    
    func configureView() {
        customStackView.stackView.addArrangedSubview(CellView(title: "Title"))
        customStackView.stackView.addArrangedSubview(CellView(title: "Tesla"))
        customStackView.stackView.addArrangedSubview(CellView(title: "Lucid"))
        customStackView.stackView.addArrangedSubview(CellView(title: "Merc"))
        customStackView.stackView.addArrangedSubview(CellView(title: "BMW"))
    }
}

CellView -- 没有变化,只是为了完成而把它包括在这里:

class CellView: UIView {
    
    private let title: UILabel = {
        let label = UILabel(frame: .zero)
        label.textColor = .black
        label.font = .systemFont(ofSize: 18, weight: .medium)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.textAlignment = .left
        label.text = "Title"
        return label
    }()
    
    private let subTitle: UILabel = {
        let label = UILabel(frame: .zero)
        label.textColor = .lightGray
        label.font = .systemFont(ofSize: 14, weight: .regular)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "subTitle"
        label.isHidden = true
        return label
    }()
    
    private let seperatorView: UIView = {
        let view = UIView(frame: .zero)
        view.backgroundColor = .lightGray
        view.heightAnchor.constraint(equalToConstant: 0.5).isActive = true
        view.translatesAutoresizingMaskIntoConstraints = false
        
        return view
    }()
    
    private let headerStackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.alignment = .leading
        stackView.distribution = .fill
        stackView.spacing = 0
        stackView.translatesAutoresizingMaskIntoConstraints = false
        return stackView
    }()
    
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }
    
    override init(frame: CGRect) {
        super.init(frame: .zero)
        
        setupView()
    }
    
    init(title: String) {
        
        super.init(frame: .zero)
        
        setupView()
        configureView(with: title)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupView() {
        
        self.backgroundColor = .gray
        
        headerStackView.addArrangedSubview(title)
        headerStackView.addArrangedSubview(subTitle)
        addSubview(seperatorView)
        
        addSubview(headerStackView)
        
        setupConstraints()
    }
    
    private func setupConstraints() {
        
        self.heightAnchor.constraint(equalToConstant: 50).isActive = true
        
        headerStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16).isActive = true
        headerStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
        headerStackView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
        
        seperatorView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
        seperatorView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
        seperatorView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
    }
    
    private func configureView(with title: String) {
        self.title.text = title
    }
    
}