Warm tip: This article is reproduced from serverfault.com, please click

StackViews in StackViews Fill All Equally Based On Smallest Item

发布于 2020-11-24 22:55:07

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

Questioner
StonedStudio
Viewed
0
DonMag 2020-12-01 05:04:31

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