0

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
    }
}
Alex Bollbach
  • 4,370
  • 9
  • 32
  • 80
  • You're missing a constraint to pin the bottom of the scroll view to the bottom of the view. E.g. `scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true`. I'd also just like to thank you for putting a self contained, easily reproducible, example of the bug. This makes the question so much more approachable. – paulvs Jul 18 '17 at 20:51
  • On a side note, when the keyboard is shown, the usual pattern is to set the `contentInset` property of the scroll view to make room for the keyboard. – paulvs Jul 18 '17 at 20:57
  • but when i add that constraint (on line 39 - https://gist.github.com/alexbollbach/38b96dec87f6a04df0eab63c299b56ad), I still see `'Scrollable content size is ambiguous for UIScrollView'` in the visual debugger. Also if you run this and tap on the yellow textview it reproduces my main problem. That is that when the scroll view's bottom constraint is animated to where the scroll view's bounds is shorter than the stack view contents, scrolling does not work. – Alex Bollbach Jul 18 '17 at 22:32
  • I looked at the gist and you've modified the line where `scrollViewBottomConstraint` is set (now pinning the bottom of the scroll view to `self.view` instead of `stackView`). That change is not necessary, you can simply add the line of code I put in my first comment and it works. – paulvs Jul 18 '17 at 22:42
  • About the keyboard, don't adjust `scrollViewBottomConstraint`, I'm quite sure this is what is breaking your constraints when you tap the yellow view. Just [adjust the contentInset property of the scrollView](https://stackoverflow.com/a/32583809/1305067). – paulvs Jul 18 '17 at 22:48
  • Can you share a screenshot of the visual debugger? I'm not sure if it's the Debug View Hierarchy button that I'm familiar with of if there is another that I'm unfamiliar with. – paulvs Jul 18 '17 at 22:54
  • that "ambiguous.." purple warning shown in the visual debugger is there from app launch before i tap the textview. also re: just adding your line of code, wouldn't that add too bottom constraints? are you saying remove the scrollView.bottom == stackview.bottom and replace it with yours? – Alex Bollbach Jul 18 '17 at 22:55
  • i have noticed that some purple warnings in the visual debugger i see in Xcode 8 do not appear in Xcode 9 so there is some variability across version for that. – Alex Bollbach Jul 18 '17 at 22:56
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/149531/discussion-between-paulvs-and-alex-bollbach). – paulvs Jul 18 '17 at 22:56

1 Answers1

0

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
    }
}
paulvs
  • 11,963
  • 3
  • 41
  • 66