UIStackView with alignment = .center crashes in UICollectionViewListCell

I'm trying to create custom cells with UIStackView and UICollectionViewListCell.

  • The layout is simple. The stack view's edges are pinned to content view's layout margins guide.
  • The stack view's axis is .horizontal
  • The stack view's alignment is .center
  • The stack view has only one arranged subview (a text label).

There are two ways to implement such cells:

Traditional Cell

Subclass UICollectionViewCell or UICollectionViewListCell and layout views in it's contentView property directly.

However, stackView.alignment = .center case the stack view renders in wrong size. It's height a bit larger that it's content.

If I add a second view to the stack view, for example an empty UIView, the layout works again.

New Content Configuration API

Use UIContentView and UIContentConfiguration to create a custom content view.

However, with stackView.alignment = .center, the app crashes.

Thread 1: "Content view returned an invalid size {353, 1.7976931348623157e+308} from -systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority: which is not allowed. If you have implemented a custom content view, you need to add constraints inside it so that its size is not ambiguous, or you need to manually compute and return a valid size. Content view: <Test.TextLabelContentView: 0x143f0e8a0; frame = (0 0; 353 44); gestureRecognizers = <NSArray: 0x600000f9edc0>; layer = <CALayer: 0x6000001d3d40>>"

If I add a second view to the stack view, for example an empty UIView, the layout works again and the app does not crash.

Reproduce

You can reproduce this problem with the attachment HomeViewController.swift

How does UIStackView determine it's size

We create a horizontal stack view whose alignment is center. When it has only one arranged subview, the stack view can't determine it's own height.

If such a stackview is in a UIContentView, the app will crash. If it's in a tranditional subclass cell, the stackview's height is incorrect (a bit larger than it's content).

If such a stackview has more than one arranged subvies, everything works great.

Is this expected?

let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .center

The behavior of cell's systemLayoutSizeFitting

The app crashes because systemLayoutSizeFitting(_:withHorizontalFittingPriority:verticalFittingPriority:) returns an invalid value.

Then I override this method in the cell and find that the targetSize parameter passed to this method is (353.0, 1.7976931348623157e+308). The height is 1.7976931348623157e+308, but why? It looks strange to me.

I thought the value should be UIView.layoutFittingCompressedSize.height.

override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
    // `targetSize` is (353.0, 1.7976931348623157e+308)
    return super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
  }

I was running through the same issue when it came to embedding a UIStackView with a horizontal axis and center alignment in a UICollectionViewCell, it turns out it is an issue with UIStackView in general not just when it is inside a UICollectionViewCell

I figured out a way to fix the issue by adding an extra arrangedSubview to the UIStackView and giving it a width of 0 and a height of 0, coupled with 0 spacing, making the view invisible. This resolves the issue of the height ambiguity for some reason. Here's the test viewcontroller I used to test this theory. Every time you press the button it flips the state from the size being 1.7976931348623157e+308 to the actual height

class ViewController: UIViewController {
    
    let stackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .horizontal
        stackView.alignment = .center
        return stackView
    }()
    
    let label = UILabel()
    
    let someView = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.center = view.center
        label.textAlignment = .center
        label.backgroundColor = .orange
        label.text = "This is text"
        stackView.addArrangedSubview(label)
        let button = UIButton(primaryAction: UIAction(handler: { [weak self] action in
            self?.buttonPressed()
        }))
        button.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button)
        NSLayoutConstraint.activate([
            stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.topAnchor.constraint(equalTo: stackView.bottomAnchor),
            someView.heightAnchor.constraint(equalToConstant: 0),
            someView.widthAnchor.constraint(equalToConstant: 0),
        ])
        button.setTitle("Flip State", for: .normal)
        
    }
    
    func buttonPressed() {
        if someView.superview != nil {
            stackView.removeArrangedSubview(someView)
            someView.removeFromSuperview()
        } else {
            stackView.addArrangedSubview(someView)
        }
        let size = stackView.systemLayoutSizeFitting(.init(width: view.frame.width, height: .greatestFiniteMagnitude),
                                                     withHorizontalFittingPriority: .required,
                                                     verticalFittingPriority: .fittingSizeLevel)
        label.text = "\(size)"
    }
    
}

I hope this helps someone out there cause I just wasted a full day's work on something as stupid as this

UIStackView with alignment = .center crashes in UICollectionViewListCell
 
 
Q