Search code examples
swiftuikituistackview

StackViews in StackViews Fill All Equally Based On Smallest Item


I'm trying to fill an unknown, but calculated number of stackViews, filled with an unknown but calculated number of UIImageViews, equally, with equal spacing.

I have a main, vertical stackView that has a height and width constraint of 75. Alignment: Center; Distribution: Fill Equally.

For Each subStackView:

        newStack.axis = .horizontal
        newStack.alignment = .center
        newStack.distribution = .fillEqually
        newStack.spacing = 1
        newStack.sizeToFit()
        mainStack.addArrangedSubview(newStack)

I then have a different loop that goes through and adds the correct number of imageViews into each subStackView:

            let image = UIImageView()
            image.contentMode = .scaleAspectFit
            image.image = UIImage(systemName: R.Image.System.circle)
            subStackView.addArrangedSubview(image)

Below, is a screenshot of the result. It appears that all the imageViews are the same size, but for some reason they appear to be spaced out equally rather than have a spacing of 1.

The bottom 3 images should all be pentagons, but as you can see, the more items that are added, the more distorted the shapes become.

enter image description here


Solution

  • This is a little tricky...

    We need to allow the "dot rows" (horizontal stack views) to center themselves, rather than stretching the width of the main stack view.

    We also need to actually calculate the dot widths instead of using .fillEqually so we don't end up with parital-point sizes.

    For example:

    • main stack width is 75
    • with 1-pt spacing
    • 3 horizontal dots

    Available width is stack view width minus number of "spaces" x spacing:

    75 - 2 = 73
    73 / 3 = 24.333333...
    

    On a @2x scale device, the actual widths of the 3 views will be:

    24.5 : 24 : 24.5
    

    Not very noticeable with just 3 "dots" but it becomes very noticeable when we get to a 1, 3, 5, 7, 9, 8, 7, 6, 5 pattern.

    .fillEqually is on the left, calculated whole-number point sizes on the right:

    enter image description hereenter image description here

    Here's some example code:

    class DotsViewController: UIViewController {
        
        let patterns: [[Int]] = [
            [3, 3, 3],
            [3],
            [1, 2, 3],
            [1, 3, 5, 4, 3],
            [1, 3, 5, 7, 9, 8, 7, 6, 5],
        ]
        
        let mainStack: UIStackView = {
            let v = UIStackView()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.axis = .vertical
            v.alignment = .fill
            v.distribution = .equalSpacing
            return v
        }()
        
        // space between dots
        let dotSpacing: CGFloat = 1
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.addSubview(mainStack)
            
            let g = view.safeAreaLayoutGuide
            
            NSLayoutConstraint.activate([
                // constrain main stack view 20-pts from top / leading / bottom
                mainStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
                
                // width: 75
                mainStack.widthAnchor.constraint(equalToConstant: 75.0),
            ])
            
            patterns.forEach { a in
                // create a vertical stack view
                //  to hold the "rows" of horizontal stack views (containing the dots)
                let vBlockStack = UIStackView()
                vBlockStack.axis = .vertical
                vBlockStack.alignment = .center
                vBlockStack.distribution = .fill
                vBlockStack.spacing = dotSpacing
                // add it to the main stack view
                mainStack.addArrangedSubview(vBlockStack)
    
                // calculate dot size
                //  needs to be a whole number so we don't get
                //  half-point sizes
                let maxDots:CGFloat = CGFloat(a.max()!)
                let availableWidth:CGFloat = 75.0 - ((maxDots - 1) * dotSpacing)
                let w:CGFloat = floor(availableWidth / maxDots)
                
                a.forEach { numDots in
                    // create a horizontal stack view
                    let hDotStack = UIStackView()
                    hDotStack.axis = .horizontal
                    hDotStack.alignment = .fill
                    hDotStack.distribution = .fill
                    hDotStack.spacing = dotSpacing
                    // add it to the vertical block stack view
                    vBlockStack.addArrangedSubview(hDotStack)
                    for _ in 0..<numDots {
                        let v = UIImageView()
                        v.contentMode = .scaleAspectFit
                        v.image = UIImage(systemName: "circle.fill")
                        v.tintColor = .red
                        // add view to dot stack view
                        hDotStack.addArrangedSubview(v)
                        // set dots image view size constraints
                        v.widthAnchor.constraint(equalToConstant: w).isActive = true
                        v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
                    }
                }
                
            }
            
        }
    
    }
    

    That produces this output:

    enter image description here