1

Currently I have a custom view (returns a UIStakView) that contains many views (UILabel, UIImageView, ...). It displays fine - on devices with plenty of height.

(BTW, this is all done programmatically.)

On small-screen devices it will only show the top part of the entire view. So my solution is to place it inside a UIScrollView. (This should be simple - but it's giving me lots of grief.)

But this won't display at all, what am I doing wrong / have missed?

Partial code below:

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

    imageFrame.addSubview(prodImage)
    NSLayoutConstraint.activate([
        prodImage.topAnchor.constraint(equalTo: imageFrame.topAnchor),
        prodImage.trailingAnchor.constraint(equalTo: imageFrame.trailingAnchor),
        prodImage.leadingAnchor.constraint(equalTo: imageFrame.leadingAnchor),
        prodImage.bottomAnchor.constraint(equalTo: imageFrame.bottomAnchor),
        ])

    imageView.addSubview(imageFrame)
    NSLayoutConstraint.activate([
        imageFrame.topAnchor.constraint(equalTo: imageView.topAnchor),
        imageFrame.trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
        imageFrame.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
        imageFrame.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
        ])

// More views...

    let stack = UIStackView(arrangedSubviews: [imageView, prodName, prodPrice])
    stack.axis = .vertical
    stack.spacing = (self.frame.height > 400) ? (self.frame.height > 800) ? 15 : 10 : 5
    stack.distribution = UIStackViewDistribution.fill

    self.addSubview(stack)
    NSLayoutConstraint.activate([
        stack.leadingAnchor.constraint(equalTo: self.leadingAnchor),
        stack.topAnchor.constraint(equalTo: self.topAnchor),
//      stack.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -50),
        stack.widthAnchor.constraint(equalTo: self.widthAnchor),
        ])
}

To make changes, I replaced the bottom stanza:

//  self.addSubview(stack)
//  NSLayoutConstraint.activate([
//      stack.leadingAnchor.constraint(equalTo: self.leadingAnchor),
//      stack.topAnchor.constraint(equalTo: self.topAnchor),
////        stack.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -50),
//      stack.widthAnchor.constraint(equalTo: self.widthAnchor),
//      ])
    let scrollView = UIScrollView()
    //      scrollView.translatesAutoresizingMaskIntoConstraints = false
    scrollView.addSubview(stack)
    NSLayoutConstraint.activate([
        stack.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
        stack.topAnchor.constraint(equalTo: scrollView.topAnchor),
        stack.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -50),
        stack.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
        ])

    self.addSubview(scrollView)
    NSLayoutConstraint.activate([
        scrollView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
        scrollView.topAnchor.constraint(equalTo: self.topAnchor),
        scrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -50),
        scrollView.widthAnchor.constraint(equalTo: self.widthAnchor),
        ])

As you can see, I tried disabling auto-constraints for the scroll view to make it fit the it's parent... All attempts failed.

How can I make this scroll view visible?

1 Answers1

4

Possible mistake:

  • You are setting the stack view's leading / trailing to the scroll view.
  • If you print the frame's you might understand that the width is zero

This is because that:

  • stack view's width can't be determined based on the scroll view.
  • scroll view is a special view because it's content can be larger than the scroll view.
  • so you need to explicitly set the content view's (stack view's) width

Possible Fix 1:

Instead of setting it based on the scrollView set it on the view (assuming scrollView is added as a subview to viewController's view)

    stack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    stack.topAnchor.constraint(equalTo: view.topAnchor),

Possible Fix 2:

You set the stack view's width anchor explicitly

Example:

Given below is a simple example of how to use stack view with the scroll view.

Your broad idea is correct.

  • Scroll View has a stack view
  • The stack view has a few subviews

Screen Shot:

ScreenShot of the code

General Explanation:

  • Scroll view is special because a scroll view's content can be wider and taller than the scroll view itself (allowing it to scroll)
  • So the content's width and height should not be tied to the scroll view
  • The content's width and height should be set without the scroll view having any part to play

Strategy

  • As you have pointed out, I like to use a Scroll view and content view
  • Add the actual content to the stack view and let the stack view grow
  • So as long as the stack view's constraints to the scroll view are set properly things should fall in place.

Debugging:

  • Always print the frame values in viewDidAppear to see if things match your expectation

Example Code:

class ViewController: UIViewController {

    let scrollView  = UIScrollView()
    let contentView = UIStackView()

    let redView     = UIView()
    let greenView   = UIView()
    let yellowView  = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()

        setupScrollView()
        setupContentView()
        setupRedView()
        setupGreenView()
        setupYellowView()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        print("scroll view  = \(scrollView.frame)")
        print("content view = \(contentView.frame)")
        print("red view     = \(redView.frame)")
        print("green view   = \(greenView.frame)")
        print("yellow view  = \(yellowView.frame)")
    }

    private func setupScrollView() {

        scrollView.backgroundColor = .darkGray

        view.addSubview(scrollView)

        scrollView.translatesAutoresizingMaskIntoConstraints = false

        scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
    }

    private func setupContentView() {

        contentView.axis            = .vertical
        contentView.distribution    = .fill
        contentView.alignment       = .fill

        scrollView.addSubview(contentView)

        contentView.translatesAutoresizingMaskIntoConstraints = false

        //Strategy is:
        //content view's leading / trailing anchors are set to view controller's view
        //content view's top / bottom anchors are set to scroll view

        contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
        contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
    }

    private func setupRedView() {

        redView.backgroundColor = .red

        redView.heightAnchor.constraint(equalToConstant: 400).isActive = true

        contentView.addArrangedSubview(redView)
    }

    private func setupGreenView() {

        greenView.backgroundColor = .green

        greenView.heightAnchor.constraint(equalToConstant: 400).isActive = true

        contentView.addArrangedSubview(greenView)
    }

    private func setupYellowView() {

        yellowView.backgroundColor = .yellow

        yellowView.heightAnchor.constraint(equalToConstant: 400).isActive = true

        contentView.addArrangedSubview(yellowView)
    }
}
user1046037
  • 16,755
  • 12
  • 92
  • 138
  • Nice explanation! What you say makes perfect sense. My main difference, is I'm implementing a custom view that inherites from a UIView - so I can't use `view` instead I have `self`. I replaced the constraints like you mentioned in Possible Fix 1., but it cause a crash. – Yuma Technical Inc. Jun 17 '18 at 17:37
  • If it crashes, see why it crashes, normally crashes tell you why they crashed. Is it because the view you added to constraints wasn't part of the view hierarchy ? It is a good practice to let the parent view / container view to determine the position of it's subviews. Each subview can set's its own constraints but let it not be aware of it's parent view – user1046037 Jun 18 '18 at 00:41