Alex Bollbach
Alex Bollbach

Reputation: 4570

Scroll View height/size is always ambiguous when containing Stack View

My goal is to create a Stack View which can house an arbitrary (~5-10) arranged subviews and become scrollable should its height become taller than the view its contained in. For this I use a scroll view.

My understanding is that a Stack View whose distribution is set to fill should have an intrinsic content size if each arranged subview has an explicit height constraint. So I can add the Stack View to a scroll view and the scroll can get its content size from the intrinsic content size of the Stack View.

I'm also trying to make this Scrolling Stack View robust to changes in frame (from the keyboard).

I've spent a great deal of time and read many long articles on Scroll View and StackView and all of the programming guides but cannot get it to work perfectly.

The following code and slight variations on it always gives a content size is ambiguous or Height is ambiguous for Scroll View. When the keyboard pops up (from tapping on the textview in the Stack View) I just subtract 400 from the Scroll View's bottom constraint's constant. My thinking is that the Scroll View's frame/bounds will then become smaller than the Stack View's intrinsic content height and scrolling will occur. However, the screen just goes blank. No constraint logs in the console either.

I've spent a very large amount of time thinking through all the considerations in this scenario but it just seems beyond me. I'd be very grateful for any help or pointers on the subject of Stack View's in Scroll Views.

Here is my current experiment with it:

class ViewController: UIViewController {

    let scrollView = UIScrollView()
    var scrollViewBottomConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
        super.viewDidLoad()

        // stack view setup (one blue and hellow view at 100 height)
        let stackView = UIStackView()
        stackView.distribution = .fill
        stackView.axis = .vertical
        let v1 = UIView()
        v1.backgroundColor = .blue
        let v2 = UITextView()
        v2.backgroundColor = .yellow
        stackView.addArrangedSubview(v1)
        stackView.addArrangedSubview(v2)


        // scroll
        scrollView.addSubview(stackView)

        view.addSubview(scrollView)

        // constraints for stack view arranged views
        v1.heightAnchor.constraint(equalToConstant: 100).isActive = true
        v2.heightAnchor.constraint(equalToConstant: 100).isActive = true

        // pin scroll view in main view
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
        scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
        scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
        // pin scroll view to stack view's bottom anchor
        scrollViewBottomConstraint = scrollView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 0)
        scrollViewBottomConstraint.isActive = true


        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 0).isActive = true
        stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: 0).isActive = true
        stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0).isActive = true


        // constrain "content view to main view and not scroll view."
        stackView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1).isActive = true


        NotificationCenter.default.addObserver(self,
                                               selector: #selector(ViewController.handleKeyboard),
                                               name: Notification.Name.UIKeyboardWillShow,
                                               object: nil)
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(ViewController.handleKeyboard),
                                               name: Notification.Name.UIKeyboardWillHide,
                                               object: nil)


    }

    func handleKeyboard(notification: Notification) {

        scrollViewBottomConstraint.constant = -400
    }
}

Upvotes: 0

Views: 1270

Answers (1)

paulvs
paulvs

Reputation: 12053

I'm adding here my working example:

class ViewController: UIViewController {

    let scrollView = UIScrollView()
    var scrollViewBottomConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
        super.viewDidLoad()

        // stack view setup (one blue and hellow view at 100 height)
        let stackView = UIStackView()
        stackView.distribution = .fill
        stackView.axis = .vertical
        let v1 = UIView()
        v1.backgroundColor = .blue
        let v2 = UITextView()
        v2.backgroundColor = .yellow
        let v3 = UITextView()
        v3.backgroundColor = .green
        let v4 = UITextView()
        v4.backgroundColor = .brown
        stackView.addArrangedSubview(v1)
        stackView.addArrangedSubview(v2)
        stackView.addArrangedSubview(v3)
        stackView.addArrangedSubview(v4)


        // scroll
        scrollView.addSubview(stackView)

        view.addSubview(scrollView)

        // constraints for stack view arranged views
        v1.heightAnchor.constraint(equalToConstant: 200).isActive = true
        v2.heightAnchor.constraint(equalToConstant: 200).isActive = true
        v3.heightAnchor.constraint(equalToConstant: 180).isActive = true
        v4.heightAnchor.constraint(equalToConstant: 250).isActive = true

        // pin scroll view in main view
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
        scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
        scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
        scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 100).isActive = true
        // pin scroll view to stack view's bottom anchor
        scrollViewBottomConstraint = scrollView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 0)
        scrollViewBottomConstraint.isActive = true


        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 0).isActive = true
        stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: 0).isActive = true
        stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0).isActive = true


        // constrain "content view to main view and not scroll view."
        stackView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1).isActive = true


        NotificationCenter.default.addObserver(self,
                                               selector: #selector(ViewController.handleKeyboard),
                                               name: Notification.Name.UIKeyboardWillShow,
                                               object: nil)
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(ViewController.handleKeyboard),
                                               name: Notification.Name.UIKeyboardWillHide,
                                               object: nil)


    }

    func handleKeyboard(notification: Notification) {

        scrollViewBottomConstraint.constant = -400
    }
}

Upvotes: 0

Related Questions