Background
I am making a vertical label to use with traditional Mongolian script. Before I was just rotating a UILabel but there were some performance issues and other complications with this. Now I am working on making a label from scratch. However, I need the vertical label to tell auto layout when its height adjusts (based on string length).
What I have read
I read the Intrinsic Content Size and Views with Intrinsic Content Size documentation. These were more about how to use it, though, and not how to define it in a custom view.
Searching for "ios intrinsic content size for a custom view" only gives me
Proper usage of intrinsicContentSize and sizeThatFits: on UIView Subclass with autolayout
in Stack Overflow. This particular question didn't even need intrinsic content size because their view was just an assembly of standard views.
What I am trying
What I am trying is my answer below. I am adding this Q&A pair so that it won't take other people as long to find the answer as it took me with the search keywords that I used.
Setting the intrinsic content size of a custom view lets auto layout know how big that view would like to be. In order to set it, you need to override intrinsicContentSize.
override var intrinsicContentSize: CGSize {
return CGSize(width: x, height: y)
}
Then call
invalidateIntrinsicContentSize()
Whenever your custom view's intrinsic content size changes and the frame should be updated.
Notes
Swift 3 update: Easier Auto Layout: Coding Constraints in iOS 9
Just because you have the intrinsic content size set up in your custom view doesn't mean it will work as you expect. Read the documentation for how to use it, paying special attention to Content-Hugging and Compression-Resistance.
Thanks also to this Q&A for putting me on the right track: How can I add padding to the intrinsic content size of UILabel?
Thanks also to this article and the documentation for help with invalidateIntrinsicContentSize().
Example of a "view with intrinsic height" ...
#IBDesignable class HView: UIView {
#IBInspectable var height: CGFloat = 100.0
override var intrinsicContentSize: CGSize {
return CGSize(width: 99, height: height)
// if using in, say, a vertical stack view, the width is ignored
}
override func prepareForInterfaceBuilder() {
invalidateIntrinsicContentSize()
}
}
which you can set as an inspectable
Since it has an intrinsic height, it can (for example) be immediately inserted in a stack view in code:
stack?.insertArrangedSubview(HView(), at: 3)
In contrast, if it was a normal view with no intrinsic height, you'd have to add a height anchor or it would crash:
let v:UIView = HView()
v.heightAnchor.constraint(equalToConstant: 100).isActive = true
stack?.insertArrangedSubview(v, at: 3)
Note that in ...
the important special case of a stack view:
you set only ONE anchor (for vertical stack view, the height; for horizontal the width)
so, setting the intrinsic height works perfectly, since:
the intrinsic height indeed means that the height anchor specifically will be set automatically if needed.
Remembering that in all normal cases of a subview, many other anchors are needed.
Related
TD;DR
It seems that in some cases systemLayoutSizeFitting does not return the correct height to correctly show / position all subviews of a view. Am I using systemLayoutSizeFitting wrong or is there some other way to avoid this?
Long story:
The XIB file of a UIViewController does not only contain the main view but also a number of other views which are added to the view controllers view at runtime. All these additional views should get the same height when they are added to the view controllers view.
The views might look like this: A simple container view holding some subviews which are stacked on top of each other.
Since the height of the container view should be flexible, the vertical spacing between the bottom button and the lable above it, uses a grater-than constraint.
To give all views the same height, I tried to measure the necessary height of each view using systemLayoutSizeFitting:
#IBOutlet var pageViews: [UIView]!
override func viewDidLoad() {
super.viewDidLoad()
var maxHeight: CGFloat = 0
for pageView in pageViews {
// Add pageView somewhere on view and give it leading, trailing and top
// constraint, but no height constraint yet.
addToView(pageView)
maxHeight = max(maxHeight, pageView.systemLayoutSizeFitting(CGSize(width: view.frame.width, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel).height)
}
for pageView in pageViews {
// Give all pageViews the same height
pageView.heightAnchor.constraint(equalToConstant: maxHeight).isActive = true
}
}
This does not work, when the label text becomes to long:
In the right example the height is not large enough and thus the button is squeezed. I can counter act this by raising the vertical compression resistance of the button, however in this case the other controls (e.g. the title label) is squeezed...
Why is this? Why does not systemLayoutSizeFitting return a height which is sufficent to show all controls without any squeezing?
Its actually smash button's height when label text is getting bigger . You are setting top and bottom constraints but button height is not declared so when label getting bigger , view basically say "I can reduce button height before updating my height , I have space.Bottom and top constraints are still same , didn't effect."
Giving the constant height constraints of button might be fix your issue.
If you want your view to resist to compression you should use the defaultHigh priority as a verticalFittingPriority instead of fittingSizeLevel.
This is a problem that has been bugging me for quite some time.
Assume a view that holds a tableView with X items. The goal is to make that view resize so that it is as high as the contents of the tableView.
An approach
Calculate the contents of the tableView in total ( e.g if there are 5 rows and each is 50 units high, its just a multiplication matter ). Then set the tableView constrained at a 0 0 0 0 into the view and set the view height to 250.
This works well for fixed height cell sizes. However!
a) How would the problem be approached for dynamic height cells though with complex constraints in a scenario where resizing happens automatically and the tableHeightForRow is set to UITableViewAutomaticDimension?
b) An idea could be using tableView.contentSize. However when would we retrieve that value safely in order to set the parent view frame accordingly? Is that even possible?
Thanks everyone
If you have a UITableView subclass, you can set up a property observer on the contentSize like this:
override var contentSize: CGSize {
didSet {
// make delegate call or use some other mechanism to communicate size change to parent
}
}
The most straightforward approach to this in my opinion is to use Autolayout. If you take this approach, you can use the contentSize to automatically invalidate the intrinsicContentSize which is what autolayout uses to dynamically size elements (as long as they don't have higher priority placement constraints restricting or explicitly setting their size).
Something like this:
override var contentSize: CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
return contentSize
}
Then, just add your table view to your parent view hierarchy with valid placement constraints and a content hugging/compression resistance of required.
How can I get the height of my custom control?
The idea is I will use it to dynamically set the height of some buttons inside the custom control. I've set the Placeholder height to 44 in the Xcode size inspector.
Working off Apple's Start Developing iOS Apps (Swift) tutorial, I am attempting to access frame.size.height and it gives a value of 1000 while the tutorial seems to suggest it should be 44.
class RatingControl: UIView {
...
override public var intrinsicContentSize: CGSize {
let buttonSize = Int(frame.size.height)
print(buttonSize) // prints 1000
let width = (buttonSize * starCount) + (spacing * (starCount - 1))
return CGSize(width: width, height: buttonSize)
}
...
You should never access frame inside intrinsicContentSize. intrinsicContentSize should return the size that perfectly fits the contents of the view, regardless of its current frame.
In your case, I think you can just use 44 for your buttonSize.
The placeholder intrinsic size is just that, placeholder, so that IB interpreter is has some value to work with and can layout the rest of the scene. But in your intrinsicContentSize getter, you implement the real size, which will be used in runtime by the AutoLayout engine. Since you return 1000 as the intrinsic content height, that's what you will see in runtime.
Background
I am making a vertical label to use with traditional Mongolian script. Before I was just rotating a UILabel but there were some performance issues and other complications with this. Now I am working on making a label from scratch. However, I need the vertical label to tell auto layout when its height adjusts (based on string length).
What I have read
I read the Intrinsic Content Size and Views with Intrinsic Content Size documentation. These were more about how to use it, though, and not how to define it in a custom view.
Searching for "ios intrinsic content size for a custom view" only gives me
Proper usage of intrinsicContentSize and sizeThatFits: on UIView Subclass with autolayout
in Stack Overflow. This particular question didn't even need intrinsic content size because their view was just an assembly of standard views.
What I am trying
What I am trying is my answer below. I am adding this Q&A pair so that it won't take other people as long to find the answer as it took me with the search keywords that I used.
Setting the intrinsic content size of a custom view lets auto layout know how big that view would like to be. In order to set it, you need to override intrinsicContentSize.
override var intrinsicContentSize: CGSize {
return CGSize(width: x, height: y)
}
Then call
invalidateIntrinsicContentSize()
Whenever your custom view's intrinsic content size changes and the frame should be updated.
Notes
Swift 3 update: Easier Auto Layout: Coding Constraints in iOS 9
Just because you have the intrinsic content size set up in your custom view doesn't mean it will work as you expect. Read the documentation for how to use it, paying special attention to Content-Hugging and Compression-Resistance.
Thanks also to this Q&A for putting me on the right track: How can I add padding to the intrinsic content size of UILabel?
Thanks also to this article and the documentation for help with invalidateIntrinsicContentSize().
Example of a "view with intrinsic height" ...
#IBDesignable class HView: UIView {
#IBInspectable var height: CGFloat = 100.0
override var intrinsicContentSize: CGSize {
return CGSize(width: 99, height: height)
// if using in, say, a vertical stack view, the width is ignored
}
override func prepareForInterfaceBuilder() {
invalidateIntrinsicContentSize()
}
}
which you can set as an inspectable
Since it has an intrinsic height, it can (for example) be immediately inserted in a stack view in code:
stack?.insertArrangedSubview(HView(), at: 3)
In contrast, if it was a normal view with no intrinsic height, you'd have to add a height anchor or it would crash:
let v:UIView = HView()
v.heightAnchor.constraint(equalToConstant: 100).isActive = true
stack?.insertArrangedSubview(v, at: 3)
Note that in ...
the important special case of a stack view:
you set only ONE anchor (for vertical stack view, the height; for horizontal the width)
so, setting the intrinsic height works perfectly, since:
the intrinsic height indeed means that the height anchor specifically will be set automatically if needed.
Remembering that in all normal cases of a subview, many other anchors are needed.
I've a fairly difficult layout design that might be easier using nested stack views in iOS. BUT, I'm having problems controlling the size or distribution of stacks nested inside other stacks. One part of the layout, for example, looks OK-ish if I set Distribution to Fit Equally:
BUT, what I really want is the photo and container to be about 1/3 the width of the text field stack. If I set Distribution to Fit Proportionally, the stack with the image (which doesn't change size) and container spreadout and squash the text against the side of the display. Everything I read suggests to reduce the Content Compression Resistance Priority. I've tried this on the image, the container and on the stack view itself, but it doesn't do much.
Could someone please point me in the right direction to control the relative widths of stacks nested inside other stacks?
To answer your question: Could someone please point me in the right direction to control the relative widths of stacks nested inside other stacks?
...
The problem is that your top-level UIStackView is asking for the intrinsicContentSize of all its subviews when determining how to divy up extra space/squash things together, based on each of the subviews' returned intrinsicContentSize and their contentHuggingPriority/contentCompressionResistance. Unfortunately, UIStackView itself - ie your nested UIStackView - doesn't return anything useful for its own intrinsicContentSize (despite it having its own subviews, which do). So the top-level UIStackView just ploughs ahead and lays things out as though the nested one doesn't care, which is why your nested UIStackView is ending up wider than you'd like.
I've had good luck by using a simple UIStackView subclass for the nested one which returns a useful intrinsicContentSize based on its own contents' widths (for a vertical axis) or heights (for a horizonal axis), as follows:
#implementation NestedStackView
- (CGSize)intrinsicContentSize
{
CGSize size = [super intrinsicContentSize]; // returns {UIViewNoIntrinsicMetric,UIViewNoIntrinsicMetric}
for (UIView *view in self.arrangedSubviews)
if (!view.hidden) { // UIStackView ignores hidden subviews when doing its layout; so should we...
if (self.axis == UILayoutConstraintAxisVertical) {
size.width = MAX(view.intrinsicContentSize.width, size.width);
} else {
size.height = MAX(view.intrinsicContentSize.height, size.height);
}
}
return size;
}
#end
By doing so, the top-level UIStackView now takes the nested UIStackView's desired content width into account when allocating its space. [aside: I first attempted adding an explicit NSLayoutConstraint on the width of the nested UIStackView, but it was just ignored. Whereas returning an instrinsicContentSize.width worked]
I'd hope there might be a better answer, but the only effective method that I've found is to put the image into a view and pin it to the view's edges. The size of the view can then be controlled with constraints.
If anyone has a better approach, they can still answer as part of this more detailed question:
https://stackoverflow.com/questions/33875801/how-to-position-and-size-images-and-their-frames-in-nested-stack-views-in-ib-w
One easy way to do this is to use AutoLayout to set relative width constraints on your nested stack views. For example, if you want to have your left UIStackView to fill 2/3rds of the screen and your right UIStackViewto fill 1/3rd, you can use the code below.
let leftStackView = UIStackView(arrangedSubviews: [...])
leftStackView.axis = .vertical
let rightStackView = UIStackView(arrangedSubviews: [...])
rightStackView.axis = .vertical
let containerStackView = UIStackView(arrangedSubviews:
[leftStackView, rightStackView])
containerStackView.axis = .horizontal
containerStackView.distribution = .fillProportionally
containerStackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(containerStackView)
//leftStackView will be twice as wide as the rightStackView, so
//the distribution is 2/3rds to 1/3rd
leftStackView.widthAnchor.constraint(equalTo:
rightStackView.widthAnchor, multiplier: 2.0).isActive = true