How to set a custom view's intrinsic content size in Swift? - ios

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

How to corrently use UIViews systemLayoutSizeFitting to get the height to show all subviews using a given width?

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.

How to subclass init#coder for a UITextView? [duplicate]

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.

TableView resizing parent view iOS

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.

Get the intrinsic height of a custom control

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.

Where to update Autolayout constraints when size changes?

I have several UIViews laid out along the bottom of a containing UIView. I want these views to always be equal width, and always stretch to collectively fill the width of the containing view (like the emoji keyboard buttons at the bottom). The way I'm approaching this is to set equal widths to one of the views, then just update the width constraint of that view to be superviewWidth / numberOfViews which will cause all of the other views to update to that same value.
I am wondering where the code to change the constraint constant needs to go. It needs to be set before the keyboard appears on screen for the first time and update when rotating the device.
My first attempt at a solution was to place it in updateViewConstraints and calculate the width via containerView.frame.size.width. But this method is called twice upon load, the first time it calculates the values correctly, but the second time for some reason the containerView's width is 0.0. Another issue is that when rotating, the containerView's width is not the value that it will be after rotation, it's the current value before rotation. But I don't want to wait until after the rotation completes to update the constraint, because the buttons will be the original size then change which will be jarring to the user.
My question is: where is the most appropriate place to put this code? Is there a better way to calculate what the width will be? I can guarantee it will always be the exact same width as the screen width. And I am using Size Classes in Xcode 6, so willRotateToInterfaceOrientation and similar methods are deprecated.
On all classes that implement the UITraitEnvironment protocol the method traitCollectionDidChange will be called when the trait collection changes, like on rotation. This is the appropiate place to manually update the constraints when using the new Size Classes. You can also animate the transition with the method willTransitionToTraitCollection
Basic example:
class ViewController: UIViewController {
var constraints = [NSLayoutConstraint]()
func updateConstraintsWithTraitCollection(traitCollection: UITraitCollection) {
// Remove old constraints
view.removeConstraints(constraints)
// Create new constraints
}
override func willTransitionToTraitCollection(newCollection: UITraitCollection!,
withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!) {
super.willTransitionToTraitCollection(newCollection, withTransitionCoordinator: coordinator)
coordinator.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext!) in
self.updateConstraintsWithTraitCollection(newCollection)
self.view.setNeedsLayout()
}, completion: nil)
}
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection!) {
updateConstraintsWithTraitCollection(traitCollection)
}
}
Besides that I want to recommend Cartography, which is a nice library that helps to make auto layout more readable and enjoyable. https://github.com/robb/Cartography
There is no reason to update the width manually:
Place all the views with equal width in your view with no spacing in between each other
Add an equal width constraint to all of them
Add constraints with 0 width for spacing between sides and each other
Lower the priority of one or more of the equal width constraints just in case the width cannot be divided equally.
Then auto layout will handle everything for you.

Resources