iosswiftuikitintrinsic-content-size

If you want a view to force to width fill on its superview, is it actually as simple as setting intrinsic to .greatestFiniteMagnitude?


Say you have, for example, a vertical stack view. Its alignment is leading (because you want most items to shrink left).

However you have one special type of item you want to be full width.

I have always done some sort of complicated solution to achieve this:

class FullWidthThing: .. {
    
    ..
        translatesAutoresizingMaskIntoConstraints = false
        
    private lazy var fullWidth: NSLayoutConstraint = {
        guard let sv = superview else { return NSLayoutConstraint() }
        let v = widthAnchor.constraint(equalTo: sv.widthAnchor)
        v.isActive = true
        return v
    }()
    
    override func layoutSubviews() {
        if superview != nil {
            _ = fullWidth
        }
        super.layoutSubviews()
    }
}

But is it actually just as simple as setting the intrinsic with to .greatestFiniteMagnitude? Superficial testing shows this seems to work:

private lazy var setup: () = {
    translatesAutoresizingMaskIntoConstraints = false
    setContentCompressionResistancePriority(.required, for: .horizontal)
    ...
    return ()
}()

override var intrinsicContentSize: CGSize {
    var sz = super.intrinsicContentSize
    sz.width = .greatestFiniteMagnitude
    return sz
}

Should this work, or is there a problem with doing this, or is there a common way of doing this?


Solution

  • For any view, Auto-layout -- whether in a stack view or other hierarchy -- will use the .intrinsicContentSize unless something else affects the size.

    Consider this layout:

    enter image description here

    The Yellow frame is a Vertical Stack View, with Top and Leading constraints, spacing: 12 and alignment: Leading

    Each outline frame is also a vertical stack view, distribution: fill equally, spacing: 4 and alignment: Leading, containing a label and a (default) UIView.

    We cannot see the views, because they have no width.

    Each "sub" stack view has a width matching the intrinsic width of its label, and the outer stack view has a width of the widest "sub" stack view.

    Outer Stack width: 300.0 (intrinsic width of widest label)
    

    Now, let's add another sub-stack view with a label that is too wide to fit the screen:

    enter image description here

    Outer Stack width: 833.6666666666666
    

    Next, let's add a Trailing constraint to the outer stack view:

    enter image description here

    Outer Stack width: 353.0
    

    and, with the longer 4th string:

    enter image description here

    Outer Stack width: 353.0 (still)
    

    So, what happens if, instead of a default UIView we use a subclassed "wide" view:

    class WideView: UIView {
        override var intrinsicContentSize: CGSize {
            var sz = super.intrinsicContentSize
            sz.width = .greatestFiniteMagnitude
            return sz
        }
    }
    

    The value of .greatesFiniteMagnitude is 1.7976931348623157e+308 ... if we format that as a float ("%0.0f") we get a number that is 309 digits long.

    Here's what we get with the first 3 strings, NO Trailing anchor on the outer stack view:

    enter image description here

    Outer Stack width: 2777777.0 (greatest width allowed by UIKit)
    

    If we add the longer string:

    enter image description here

    It looks the same, and again:

    Outer Stack width: 2777777.0 
    

    Add the outer stack view Trailing anchor:

    enter image description here

    Outer Stack width: 353.0
    

    Here's the class I used to generate those outputs (see the comments and commented lines):

    class WideTestVC: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .systemYellow
            
            let strs: [String] = [
                "Short",
                "Medium length",
                "Longer string for width testing",
                //"This string will be too long to fit the available width of a phone in portrait orientation!",
            ]
            let colors: [UIColor] = [
                .systemRed,
                .systemGreen,
                .systemBlue,
                .cyan,
            ]
            
            let outerStack = UIStackView()
            outerStack.axis = .vertical
            outerStack.alignment = .leading
            outerStack.spacing = 12
            
            for (str, c) in zip(strs, colors) {
                let st = UIStackView()
                st.axis = .vertical
                st.alignment = .leading
                st.distribution = .fillEqually
                st.spacing = 4
                st.layer.borderColor = UIColor.darkGray.cgColor
                st.layer.borderWidth = 1
                
                let label = UILabel()
                label.font = .systemFont(ofSize: 24.0, weight: .light)
                label.text = str
                label.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
                
                // to see the difference...
                // use standard UIView (will have no width)
                let v = UIView()
                // or, use custom "WideView" with width: .greatestFiniteMagnitude
                //let v = WideView()
                
                v.backgroundColor = c
                st.addArrangedSubview(label)
                st.addArrangedSubview(v)
                outerStack.addArrangedSubview(st)
            }
            
            outerStack.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(outerStack)
            
            let g = view.safeAreaLayoutGuide
            
            // constrain ONLY Top / Leading, or
            // constrain Top / Leading / Trailing
            NSLayoutConstraint.activate([
                outerStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 30.0),
                outerStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                //outerStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            ])
            
            outerStack.backgroundColor = .yellow
        }
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let v = view.subviews.first as? UIStackView
            else { return }
            
            print(v.frame.width)
            
        }
    }