4

I'm trying to make a UIScrollView containing a UIStackView with several layers of stack views nested inside. I would like to use AutoLayout, but there is something wrong with what I'm doing, and my brain is getting scrambled trying to figure it out:

import UIKit

class HomeView: UIView {

    let looks = sampleLooks
    let user = sampleUser
    let streamView: UIStackView = UIStackView(arrangedSubviews: [])
    let scrollView: UIScrollView = UIScrollView()

    func makeButtonWithTitle(title: String, image: UIImage?, selector: String, tag: Int) -> UIButton {
        let button = UIButton(type: .System)
        button.setImage(image, forState: .Normal)
        button.tintColor = UIColor.blackColor()
        switch tag {
        case 0...10:
            button.backgroundColor = UIColor(white: 0.98, alpha: 0.8)
            button.titleLabel?.font = UIFont(name: "HelveticaNeue-Thin", size: 30)
        default:
            button.backgroundColor = UIColor(white: 0.90, alpha: 1.0)
            button.titleLabel?.font = UIFont(name: "HelveticaNeue-Thin", size: 40)
        }
        button.setTitle(title, forState: .Normal)
        button.tag = tag
        return button
    }

    func makeMessageView(senderImage: UIImage, senderHandle: String, text: String) -> UIView {
        let textView = UILabel()
        textView.text = text
        textView.font = UIFont(name: "HelveticaNeue-Thin", size: 20)
        let senderLabel = UILabel()
        senderLabel.text = senderHandle
        textView.font = UIFont(name: "HelveticaNeue-Thin", size: 20)

        let textStackView = UIStackView(arrangedSubviews:[senderLabel, textView])
        textStackView.axis = .Horizontal
        textStackView.alignment = .Fill
        textStackView.distribution = .Fill

        let postView = UIView()
        postView.addSubview(textStackView)
        postView.backgroundColor = UIColor(white: 0.98, alpha: 0.8)

        return postView
    }

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

        self.contentMode = .ScaleToFill
        self.backgroundColor = UIColor(patternImage: UIImage(named: "background")!)

        self.streamView.spacing = 20.0
        self.streamView.translatesAutoresizingMaskIntoConstraints = false
        self.streamView.axis = .Vertical
        self.streamView.alignment = .Fill
        self.streamView.distribution = .FillEqually

        for look in self.looks {

            let lookImageView = UIImageView(image: look.photo.photo)
            lookImageView.contentMode = .ScaleAspectFit
            lookImageView.clipsToBounds = true

            let postView = self.makeMessageView(
                look.user.photo.photo, senderHandle: look.user.handle, text: look.post)

            let likeButton = self.makeButtonWithTitle(
                " Like", image: UIImage(named: "like"), selector: "", tag: 0)
            let commentButton = self.makeButtonWithTitle(
                " Comment", image: UIImage(named: "SMS"), selector: "", tag: 1)
            let shareButton = self.makeButtonWithTitle(
                " Share", image: UIImage(named: "upload"), selector: "", tag: 2)
            let buttonsView = UIStackView(arrangedSubviews: [likeButton, commentButton, shareButton])
            buttonsView.distribution = .FillEqually

            let lookView = UIStackView(arrangedSubviews:[lookImageView, postView, buttonsView])
            lookView.axis = .Vertical
            lookView.distribution = .Fill
            lookView.alignment = .Fill
            self.streamView.addArrangedSubview(lookView)

        }

        self.scrollView.addSubview(self.streamView)
        self.scrollView.frame = UIScreen.mainScreen().bounds
        self.scrollView.showsVerticalScrollIndicator = false
        self.scrollView.showsHorizontalScrollIndicator = false
        self.addSubview(self.scrollView)

    }

}

So, what I would like this code to do is to give me a scrollable stack of nested stacks that covers the width of the screen (it's ok if the images are clipped, but I'd like them to cover the screen without distortion). What it actually gives me is a scrollable set of images where the width of the first image (seemingly) determines the width of the stack. I'm actually not sure if that's what's going on, but the top level UIStackView is not covering the width of the screen.

I think it has something to do with the UIScrollView has no intrinsic width, so the stack view inside it decides its own size. I say this because if I put the stack view directly in the parent view, it covers the display, but then as you might expect, there is no scrolling...

gred
  • 612
  • 1
  • 8
  • 15

1 Answers1

6

You need to set some constraints between the scrollview and the UIStackView it contains, at the moment you don't have any.

These three are enough to make the inner UIStackView the same size of the scrollview:

  • Horizontal constraint between the scrollview and the UIStackView, no space between them
  • Vertical constraint between the scrollview and the UIStackView, no space between them, this way you'll be able to scroll for the full height of the UIStackView
  • Same width for the scrollview and the UIStackView, this way the UIStackView will match the scrollview width

And this is the code:

//You already have these two lines:
//scrollView.addSubview(streamView)
//streamView.translatesAutoresizingMaskIntoConstraints = false;
//Add this one too:
scrollView.translatesAutoresizingMaskIntoConstraints = false;

scrollView.addConstraints(
    NSLayoutConstraint.constraintsWithVisualFormat("V:|[innerView]|", 
        options: NSLayoutFormatOptions(rawValue:0),
        metrics: nil,
        views: ["innerView":streamView]))

scrollView.addConstraints(
    NSLayoutConstraint.constraintsWithVisualFormat("H:|[innerView]|", 
        options: NSLayoutFormatOptions(rawValue:0),
        metrics: nil, 
        views: ["innerView":streamView]))

scrollView.addConstraint(
    NSLayoutConstraint(item: scrollView, 
                  attribute: .Width, 
                  relatedBy: .Equal, 
                     toItem: streamView, 
                  attribute: .Width, 
                 multiplier: 1.0, 
                   constant: 0))

Alternatively, you can extend UIView with a method that embed your view in a scrollview:

extension UIView {

    func embedInScrollView()->UIView{
        let cont=UIScrollView()

        self.translatesAutoresizingMaskIntoConstraints = false;
        cont.translatesAutoresizingMaskIntoConstraints = false;
        cont.addSubview(self)
        cont.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[innerView]|", options: NSLayoutFormatOptions(rawValue:0),metrics: nil, views: ["innerView":self]))
        cont.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[innerView]|", options: NSLayoutFormatOptions(rawValue:0),metrics: nil, views: ["innerView":self]))
        cont.addConstraint(NSLayoutConstraint(item: self, attribute: .Width, relatedBy: .Equal, toItem: cont, attribute: .Width, multiplier: 1.0, constant: 0))
        return cont
    }
}

And use it this way:

let scrollView = streamView.embedInScrollView()

Edit: Fixed last constraint in the first snippet.

uraimo
  • 19,081
  • 8
  • 48
  • 55
  • Thx @uraimo, I will try that. – gred Nov 09 '15 at 17:02
  • It should work, i have an app that has essentially the same structure you posted. – uraimo Nov 09 '15 at 17:03
  • Thanks to this code: scrollView.addConstraint( NSLayoutConstraint(item: streamView, attribute: .Width, relatedBy: .Equal, toItem: scrollView, attribute: .Width, multiplier: 1.0, constant: 0)) I'm basically there. It's not exactly what you posted (`toItem: scrollView` instead of `toItem: streamView`, but I think that's what you meant), but thanks for the nudge! – gred Nov 09 '15 at 17:31