I would like to create a custom UIView
which uses/offers a IntrinsicContentSize, since its height depends on its content (just like a label where the height depends on the text).
While I found a lot of information on how to work with IntrinsicContentSize offered by existing Views, I found just a few bits on how to use IntrinsicContentSize on a custom UIView
:
@IBDesignable class MyIntrinsicView: UIView {
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.setFillColor(UIColor.gray.cgColor)
context?.fill(CGRect(x: 0, y: 0, width: frame.width, height: 25))
height = 300
invalidateIntrinsicContentSize()
}
@IBInspectable var height: CGFloat = 50
override var intrinsicContentSize: CGSize {
return CGSize(width: super.intrinsicContentSize.width, height: height)
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
invalidateIntrinsicContentSize()
}
}
The initial high is set to 50
The draw
methods draws a gray rect with a size 25
height is changed to 300
and invalidateIntrinsicContentSize()
is called
Placing a MyView
instance in InterfaceBuilder works without any problem. The view does not need a height constraint
However, in IB the initial height of 50
is used. Why? IB draws the gray rect, thus the draw
method is called. So why is the height not changed to 300?
What is also strange: When setting a background color it is drawn as well, also super.draw(...)
is not called. Is this intended?
I would expect a view with height of 300
and a gray rect at the top with a height of 25
. However, when running the project in simulator the result is different:
300
- OKIt seems that the content was stretched from its original height of 25
to keep its relative height to the view. Why is this?
Trying to change the view's height from inside draw()
is probably a really bad idea.
First, as you've seen, changing the intrinsic content size does not trigger a redraw. Second, if it did, your code would go into an infinite recursion loop.
Take a look at this edit to your class:
@IBDesignable class MyIntrinsicView: UIView {
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.setFillColor(UIColor.gray.cgColor)
context?.fill(CGRect(x: 0, y: 0, width: frame.width, height: 25))
// probably a really bad idea to do this inside draw()
//height = 300
//invalidateIntrinsicContentSize()
}
@IBInspectable var height: CGFloat = 50 {
didSet {
// call when height var is set
invalidateIntrinsicContentSize()
// we need to trigger draw()
setNeedsDisplay()
}
}
override var intrinsicContentSize: CGSize {
return CGSize(width: super.intrinsicContentSize.width, height: height)
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
// not needed
//invalidateIntrinsicContentSize()
}
}
Now, when you change the intrinsic height in IB via the IBDesignable
property, it will update in your Storyboard properly.
Here's a quick look at using it at run-time. Each tap (anywhere) will increase the height
property by 50 (until we get over 300, when it will be reset to 50), which then invalidates the intrinsic content size and forces a call to draw()
:
class QuickTestVC: UIViewController {
@IBOutlet var testView: MyIntrinsicView!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
var h: CGFloat = testView.intrinsicContentSize.height
h += 50
if h > 300 {
h = 50
}
testView.height = h
}
}