skip to Main Content

(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).

2

Answers


  1. Chosen as BEST ANSWER

    Here's a class that allows creation of views with a specific early known intrinsic content size (similar to the default frame: constructor), but a size UIStackView honors, into which, for example, CALayer objects can be rendered after the view's creation:

    class AdjIntrinsicSizeView : UIView {
        var size : CGSize = CGSize.zero
        required init?(coder: NSCoder) {
            fatalError("init?(coder: NSCoder) unimplemented")
        }
        override init(frame: CGRect) {
            super.init(frame: frame)
        }
        convenience init(size: CGSize = CGSize.zero, frame: CGRect = CGRect.zero) {
            self.init(frame: frame);
            self.size = size
        }
        override var intrinsicContentSize: CGSize { size }
    }
    

    Usage example:

    let iconView = AdjIntrinsicSizeView(size: CGSizeMake(15, 15))
    renderDrawing(into: iconView)
    let stackView = UIStackView(arrangedItems: [iconView, ...])
    

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

    • the convenience initialiser lets me specify a size and a frame and that frame could have a different size (which is potentially useful but more likely a source of errors in my opinion). I suggested it should take an origin and a size.
    • the override of the default initialiser (taking just a frame) doesn’t save its size so the intrinsic size will be reported as .zero.

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

    • a view which reports a specific intrinsic size could be useful outside of a stack view so relying on stack-view-specific behaviour makes it less flexible.
    • I prefer APIs that are designed to make it impossible to do the wrong / inconsistent / ambiguous thing.
    • Once the default (frame) initialiser saves the initial size, there isn’t really much value in having the convenience initialiser take an origin. Similarly, anyone wanting to specify a zero size could use init() or specify a frame so the convenience initialiser could require a size without any loss of features.
    • I thought about making the intrinsic size fixed at initialisation but then decided it is probably better to allow it to be changed at run time (like the original). But if that does happen, it needs to call invalidateIntrinsicContentSize().

    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 }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search