Search code examples
swiftuiviewsnapkit

Stacking views programmatically in UIStackView


I have spent a long time trying to stack the views that I create programmatically. I looked at examples from Add views in UIStackView programmatically but that didn't work. Listed below is the code, I am calling the setUpListings from the view controller. There are two entries but only one entry is shown.

import UIKit
import SnapKit

class ListingsView : UIView {
    var containerView: UIView!
    var listingsContainerView: UIStackView!

    init() {
        super.init(frame: CGRect.zero)
        setUpContainerView()
        setUpListingsContainer()
    }

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

    func setUpContainerView() {
        containerView = UIView()

        self.addSubview(containerView)

        containerView.snp.makeConstraints { (make) in
            make.height.equalTo(self)
            make.width.equalTo(self)

            containerView.backgroundColor = UIColor.white
        }
    }

    func setUpListingsContainer() {
        listingsContainerView = UIStackView()
        listingsContainerView.distribution = .equalSpacing
        listingsContainerView.alignment = .fill
        listingsContainerView.axis = .vertical
        listingsContainerView.spacing = 10
        listingsContainerView.translatesAutoresizingMaskIntoConstraints = false

        containerView.addSubview(listingsContainerView)

        listingsContainerView.snp.makeConstraints { (make) in
            make.top.equalTo(containerView)
            make.left.equalTo(containerView)
            make.bottom.equalTo(containerView)
            make.right.equalTo(containerView)
        }
    }

    func setUpListings(listings: [Listing]) {
        for listing in listings {
            let listingEntry = ListingEntry(listingId: listing.id)
            listingsContainerView.addArrangedSubview(listingEntry)
        }
    }

    class ListingEntry : UIView {
        var listingId: String?
        var containerView: UIView!

        init(listingId: String) {
            super.init(frame: CGRect.zero)
            self.listingId = listingId
            self.setUpContainerView()
        }

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

        func setUpContainerView() {
            containerView = UIView()
            containerView.backgroundColor = UIColor.gray
            self.addSubview(containerView)

            containerView.snp.makeConstraints { (make) in
                make.width.equalTo(150)
                make.height.equalTo(150)
            }
        }
    }
}

The view currently looks like enter image description here

But the blocks should be stacked.


Solution

  • Couple things...

    First, I'd suggest learning how constraints and auto-layout work before using something like SnapKit. It can make some things easier --- but until one has a good understanding of the fundamentals, it's not clear what's doing what.

    Second, during development, it helps to give views and subviews contrasting background colors. Makes it much easier to see what's happening to the frames at run-time.

    So, if you're going to stick with SnapKit...

    Try to keep code "clean." That is, don't put anything inside a snp.makeConstraints block that isn't directly related (such as setting background colors).

    In your ListingEntry class, you're adding a subview (containerView) and giving that view a width and height of 150, but you are not constraining it to its superview... which results in a view height of Zero.

    Take a look at the modifications I made to your code. I added comments that should make the changes clear:

    class MiscViewController: UIViewController {
    
        var listingsView: ListingsView = {
            let v = ListingsView()
            return v
        }()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            view.addSubview(listingsView)
    
            listingsView.backgroundColor = .red
    
            // constrain listingsView to all 4 sides with 40-pt "padding"
            listingsView.snp.makeConstraints { (make) in
                make.top.bottom.leading.trailing.equalToSuperview().inset(40.0)
            }
    
            let listings: [Listing] = [
                Listing(id: "A"),
                Listing(id: "B"),
                Listing(id: "C"),
            ]
    
            listingsView.setUpListings(listings: listings)
    
        }
    
    }
    
    struct Listing {
        var id: String = ""
    }
    
    class ListingsView : UIView {
        var containerView: UIView!
        var listingsContainerView: UIStackView!
    
        init() {
            super.init(frame: CGRect.zero)
            // probably want to set clipsToBounds so any content doesn't extend outside the frame
            clipsToBounds = true
            setUpContainerView()
            setUpListingsContainer()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
    
        func setUpContainerView() {
            containerView = UIView()
    
            self.addSubview(containerView)
    
            containerView.backgroundColor = UIColor.green
    
            // constrain containerView to all 4 sides
            containerView.snp.makeConstraints { (make) in
                make.top.bottom.leading.trailing.equalToSuperview()
            }
        }
    
        func setUpListingsContainer() {
            listingsContainerView = UIStackView()
            listingsContainerView.distribution = .equalSpacing
            listingsContainerView.alignment = .fill
            listingsContainerView.axis = .vertical
            listingsContainerView.spacing = 10
    
            containerView.addSubview(listingsContainerView)
    
            // constrain listingsContainerView (a stack view) to all 4 sides
            listingsContainerView.snp.makeConstraints { (make) in
                make.top.leading.bottom.trailing.equalToSuperview()
            }
        }
    
        func setUpListings(listings: [Listing]) {
            for listing in listings {
                let listingEntry = ListingEntry(listingId: listing.id)
                listingEntry.backgroundColor = .cyan
                listingsContainerView.addArrangedSubview(listingEntry)
            }
        }
    
        class ListingEntry : UIView {
            var listingId: String?
            var containerView: UIView!
    
            init(listingId: String) {
                super.init(frame: CGRect.zero)
                self.listingId = listingId
                self.setUpContainerView()
            }
    
            required init?(coder aDecoder: NSCoder) {
                super.init(coder: aDecoder)
            }
    
            func setUpContainerView() {
                containerView = UIView()
                containerView.backgroundColor = .gray
                self.addSubview(containerView)
    
                containerView.snp.makeConstraints { (make) in
                    // you want the "listing container" to be 150 x 150 pts
                    make.width.equalTo(150)
                    make.height.equalTo(150)
                    // and it needs top and bottom constraints to give self a height value
                    make.top.bottom.equalToSuperview()
                    // and it needs an x-position constraint
                    make.leading.equalToSuperview()
                }
            }
        }
    }
    

    I've set the "main" ListingsView background color to red ... you don't see it because its containerView subview is green and fills the view.

    Each ListingEntry view has a cyan background color, and its containerView has a gray background color.

    The result:

    enter image description here

    and Debug View Hierarchy:

    enter image description here

    Last notes...

    • You set your StackView .distribution = .equalSpacing but you also set .spacing = 10, which doesn't make sense.
    • If you have more ListingEntry views than will fit vertically, you'll run into problems. I'd expect you'd put that into a scroll view.