How do I control the relative widths of UIStackViews nested within another stack view? - ios

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

Related

How to set alignments right for dynamic UIStackView with inner XIB

My original ViewController consists of only one scrollView like this:
Now I also have my own xib file (CheckBoxView) which mainly consists of one button, see this screenshot:
I dynamically create some UIStackViews and add them to the ScrollView Inside these UIStackViews I add multiple instances of my xib file.
What I want to achieve is, that the StackViews are just vertically stacked. And inside the StackViews the UIViews from my xib-file should also be vertically stacked.
At the moment it looks like this:
So the xib-Views are not in the whole view. Since I am using multi-os-engine I can't provide swift/obj-c code. But here is my Java-Code:
for (ItemConfiguration config : itemInstance.getConfigurations()) {
List<DLRadioButton> radioButtons = new ArrayList<DLRadioButton>();
UIStackView configView = UIStackView.alloc().initWithFrame(new CGRect(new CGPoint(0, barHeight), new CGSize(displayWidth, displayHeight - barHeight)));
configView.setAxis(UILayoutConstraintAxis.Vertical);
configView.setDistribution(UIStackViewDistribution.EqualSpacing);
configView.setAlignment(UIStackViewAlignment.Center);
configView.setSpacing(30);
for (ConfigurationOption option : config.getOptions()) {
UIView checkBox = instantiateFromNib("CheckBoxView");
for (UIView v : checkBox.subviews()) {
if (v instanceof DLRadioButton) {
((DLRadioButton) v).setTitleForState(option.getName(), UIControlState.Normal);
//((DLRadioButton) v).setIconSquare(true);
radioButtons.add((DLRadioButton) v);
}
}
configView.addArrangedSubview(checkBox);
}
// group radiobuttons
//groupRadioButtons(radioButtons);
configView.setTranslatesAutoresizingMaskIntoConstraints(false);
scrollView().addSubview(configView);
configView.centerXAnchor().constraintEqualToAnchor(scrollView().centerXAnchor()).setActive(true);
configView.centerYAnchor().constraintEqualToAnchor(scrollView().centerYAnchor()).setActive(true);
}
private UIView instantiateFromNib(String name) {
return (UIView) UINib.nibWithNibNameBundle(name, null).instantiateWithOwnerOptions(null, null).firstObject();
}
How do I need to set the Alignments etc. to Achieve what I want. It should look like this:
I don't know if there is a reason to not use UITableView, that i highly recommend for your case. In case it's not possible, below you can find some pieces of advice that should help.
If you use Auto Layout, you should set constraints for all views instantiated in your code. The constraints must be comprehensive for iOS to know each view's position and size.
Remove redundant constraints
configView.centerXAnchor().constraintEqualToAnchor(scrollView().centerXAnchor()).setActive(true);
configView.centerYAnchor().constraintEqualToAnchor(scrollView().centerYAnchor()).setActive(true);
These two constraint just doesn't make sense to me. You need the stackviews be stacked within you ScrollView, but not centered. If i understand you goal correctly, this should be removed
Set width/x-position constraints for UIStackViews
configView.setTranslatesAutoresizingMaskIntoConstraints(false);
scrollView().addSubview(configView);
Right after a stack view is added to the ScrollView, you need to set up constraints for it. I'll provide my code in swift, but it looks quite similar to what your Java code is doing, so hopefully you'll be able to transpile it without difficulties:
NSLayoutConstraint.activate([
configView.leadingAnchor.constraintEqualToAnchor(scrollView.leadingAnchor),
configView.trailingAnchor.constraintEqualToAnchor(scrollView.trailingAnchor)
]);
Set height constraints for UIStackViews
StackViews doesn't change their size whenever you add arranged view in it. So you need to calculate a desired stackview size yourself and specify it explicitly via constraints. It should be enough to accommodate items and spaces between them. I suppose that all items should be of the same size, let it be 32 points, then height should be:
let stackViewHeight = items.count * 32 + stackView.space * (items.count + 1)
And make new height constraint for the stack view:
configView.heightAnchor.constraint(equalToConstant: stackViewHeight).isActive = true
Set y-position for UIStackView
This is a little bit more challenging part, but the most important for the views to work properly in a scroll view.
1) Change loop to know the index of a UIStackView
A scroll view should always be aware of height of its content, so you need to understand which stack view is the top one, and which is the bottom. In order to do that, you need to change for each loop to be written as for(;;) loop:
for (int i = 0; i < itemInstance.getConfigurations().length; i++) {
ItemConfiguration config = itemInstance.getConfigurations()[i]
...
}
I'm not aware of which type your array is, so if it doesn't have subscript functionality, just replace it with corresponding method.
2) Set top anchor for stack views
For the first stack view in the array, top anchor should be equal to the scroll view top anchor, for others it should be bottom anchor of the previous stack view + spacing between them (say, 8 points in this example):
if i == 0 {
configView.topAnchor.constraintEqualToAnchor(scrollView.topAnchor, constant: 8).isActive = true
} else {
let previousConfigView = itemInstance.getConfigurations()[i - 1]
configView.topAnchor.constraintEqualToAnchor(previousConfigView.bottomAnchor, constant: 8).isActive = true
}
3) Set bottom anchor for the last stack view
As was said - for the Scroll View to be aware of content size, we need to specify corresponding constraints:
if i == itemInstance.getConfigurations() - 1 {
configView.bottomAnchor.constraintEqualToAnchor(scrollView.bottomAnchor, constant: 8).isActive = true
}
Note: please be advised, that all constraints should be set on views that are already added to the scroll view.

Swift, how to add X number of views with constraints dynamically

I am developing a swift application which will fit 10 items on the screen. If I wanted to do this on a screen that would not change size i.e. the user doesn't change orientation or an iPad user does not use split screen, I would be able to detect the width by doing let size = bounds.width/19.
The problem is as the screen size is dynamic so therefor I need to do it with constraints. I would not like to use UICollectionView as that is too heavy and would also not like to use UIStackView if possible as I don't think it supports aspect ratio which I need for circles. I am trying to use UIViews.
Edit:
This is how I want them to look. This will be about 50 high and other information will be underneath.
UIStackView is the right tool for this job. In the future, I recommend more rigorously defining what you want to happen first, then dive into the documentation.
let sv = UIStackView()
sv.spacing = 10
// this means each arranged subview will take up the same amount of space
sv.distribution = .fillEqually
for _ in 0..<50 {
// omitting the rounded corners or any other styling because
// it's not the point of this question
let subview = UIView()
// The stack view will determine the width based on the screen size
// we just need to say that height == width
subview.widthAnchor.constraint(equalTo: subview.heightAnchor).isActive = true
sv.addArrangedSubview(subview)
}
// add your stackview to your view hierarchy and constrain it

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.

UIStackView; Equal Spacing Between & Outside Elements

UIStackView is awesome, I love Equal Spacing Distribution.
But how to achieve the same space also outside of elements dynamically?
In my case all elements will have same ratio 1:1
You can add equal spacing using the story board as shown here:
Source: https://stackoverflow.com/a/32862693/3393964
#Declan has the right idea. Here's that answer programatically where you add extra views on either side so the stack view gives correct outside spacing with any number of buttons.
stackView.alignment = .center
stackView.axis = .horizontal
stackView.distribution = .equalCentering
// Then when I add the views...
let leftView = UIView()
stackView.addArrangedSubview(leftView)
content.buttons.forEach { (button) in
stackView.addArrangedSubview(button)
}
let rightView = UIView()
stackView.addArrangedSubview(rightView)
Here's what my view looks like with 2 items using equalSpacing
And here it is with equalCentering distribution, also a nice look.
I prefer to let the UIStackView handle the spacing. Create a UIStackView with equal spacing and add two 0px wide (0px high if using a vertical stackview) transparent views to the the far sides of your stack view.
You can use constraints and give then same height and width. So when you change the dimension of anyone of the component then all components are changed with same dimension.
I think what you want is to have the same spacing outside of the stack view with the spacing outside.
What I would do is the put stack view inside another view (GRAY VIEW) and set the leading and trailing constraint of the stack view to be equal to the spacing of the stack view.
Spacing of the Stack View
Constraints of the Stack View from its super view (Gray View)

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

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.

Resources