iosswiftuiviewuistackviewintrinsic-content-size

Is there an easy & flexible way to set intrinsic size for UIViews?


(I'm posting this question as S.O. ("Answer your own question") format to make this recipe easy to find, and to get any other helpful perspectives).

Question: What is a convenient way to set a specific content size UIViews?

Use case driving the question:

I'm working with an app with many UIStackView instances that display custom icons next to each other.

The stack view items are often UILabel other times, simply UIView.

Apple Docs say UILabel is a type that sets its own intrinsic content size. And in many case that works out okay. However, for certain UILabel, instances, the intrinsic content size is not set optimally, which can be challenging for getting UIStackView to layout precisely.

In other cases, I create a UIView where it is not optimal to subclass each UIView I create, but rather render a CALayer into the view after the view's creation. Thus the UIView cannot set its intrinsic size properly. Thus, when I use that as an item in UIStackView, it sees the content CGSize as (0, 0), regardless of frame setting, and so UIStackView doesn't handle it properly even with spacing and custom spacing options.

Since I know the size of the layer whose graphics I'm rendering in advance, it would be helpful to be able to construct the UIView with a predetermined size, similarly to the way people generally construct UIView(frame: CGRectMake(x, y, w, h), but in a way UIStackView recognizes (e.g. intrinsic content size).


Solution

  • The OP and I briefly discussed this design and I pointed out some potential sources of ambiguity:

    Having thought about it some more, here’s a few more things I think could be improved:

    The OP is obviously happy with their version and invited me to create my own answer. Here’s a version which addresses my comments:

    class IntrinsicContentSizeView : UIView {
        var size : CGSize {
            didSet {
                invalidateIntrinsicContentSize()
            }
        }
    
        override init(frame: CGRect) {
            size = frame.size
            super.init(frame: frame)
        }
        
        convenience init(size: CGSize) {
            self.init(frame: CGRect(origin: .zero, size: size))
        }
    
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        override var intrinsicContentSize: CGSize { size }
    }