I am trying to use the new Auto Layout in Lion because it seems quite nice. But I can not find good information about how to do things. For example:
I have two labels:
I don't think you could do it that way. If you made the layout for label 2 be based on a distance constraint from label 1, even if you made label 1 auto-collapse to zero height when it has no content, label 2 is still going to be that distance down, ie in:
+----------------+
| +------------+ |
| + label 1 | |
| +------------+ |
| ^ |
| ^ !
| +------------+ |
| | label 2 | |
| +------------+ |
+----------------+
Where ^ is the autolayout distance constraint - If Label 1 knows how to become zero height when it's string is empty, you're still going to get:
+----------------+
| +------------+ |
| ^ |
| ^ !
| +------------+ |
| | label 2 | |
| +------------+ |
+----------------+
Maybe it is possible by creating your NSLayoutConstraint manually. You could make the second attribute be the height of label 1, make the constant zero, and then carefully work out what the multiplier would be to make the distance be what you want based on a multiple of the non-zero label height.
But having done all this, you've now coded an NSLabel subclass that auto-sizes, created a constraint object manually instead of via the visual language, and bent NSLayoutConstraint beyond its will.
I think you're better off just changing the frame of label 2 if label 1's string is blank!
One simple solution is to just subclass UILabel and change the intrinsic content size.
@implementation WBSCollapsingLabel
- (CGSize)intrinsicContentSize
{
if (self.isHidden) {
return CGSizeMake(UIViewNoIntrinsicMetric, 0.0f);
} else {
return [super intrinsicContentSize];
}
}
- (void)setHidden:(BOOL)hidden
{
[super setHidden:hidden];
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
@end
I've found another way to do this. This methodology can be applied anywhere, has no scaling problems; and handles the margins as well. And you don't need 3rd party things for it.
First, dont use this layout:
V:|-?-[Label1]-10-[Label2]-10-|
H:|-?-[Label1]-?-|
H:|-20-[Label2]-20-|
Use these instead:
("|" is the real (outer) container)
V:|-?-[Label1]-0-[Label2HideableMarginContainer]-0-|
H:|-?-[Label1]-?-|
H:|-0-[Label2HideableMarginContainer]-0-|
("|" is Label2HideableMarginContainer)
V:|-10-[Label2]-10-|
H:|-20-[Label2]-20-|
So what have we done now? Label2
is not directly used in the layout; it's placed into a Margin-Container
. That container is used as a proxy of Label2
, with 0 margins in the layout. The real margins are put inside of the Margin-Container
.
Now we can hide Label2
with:
Hidden
to YES
on itAND
Top
, Bottom
, Leading
and Trailing
constraints. So seek them out, than set Active
to NO
on them. This will cause the Margin-Container
to have a Frame Size
of (0,0); because it does have subview(s); but there aren't any (active) layout constraints which anchors those subviews to it.Maybe a bit complex, but you only have to develop it once. All the logic can be put into a separate place, and be reused every time you need to hide smg.
Here is C# Xamarin code how to seek those constraints which anchors the subview(s) to the inner edges of the Margin-Container
view:
public List<NSLayoutConstraint> SubConstraints { get; private set; }
private void ReadSubContraints()
{
var constraints = View.Constraints; // View: the Margin-Container NSView
if(constraints?.Any() ?? false)
{
SubConstraints = constraints.Where((NSLayoutConstraint c) => {
var predicate =
c.FirstAttribute == NSLayoutAttribute.Top ||
c.FirstAttribute == NSLayoutAttribute.Bottom ||
c.FirstAttribute == NSLayoutAttribute.Leading ||
c.FirstAttribute == NSLayoutAttribute.Trailing;
predicate &= ViewAndSubviews.Contains(c.FirstItem); // ViewAndSubviews: The View and View.Subviews
predicate &= ViewAndSubviews.Contains(c.SecondItem);
return predicate;
}).ToList();
}
}
Solved this issue programmatically. I have a few buttons in a row, and I may decide to hide one at any time.
Used Cartography to replace them each time hidden
changes in any of them.
let buttons = self.buttons!.filter { button in
return !button.hidden
}
constrain(buttons, replace: self.constraintGroup) { buttons in
let superview = buttons.first!.superview!
buttons.first!.left == superview.left
for var i = 1; i < buttons.count; i++ {
buttons[i].left == buttons[i-1].right + 10
}
buttons.last!.right == superview.right
}
This is possible with auto layout, but doesn't exactly scale well.
So, taking your example, let's say you have label A, and label B (or button or anything else really). First start by adding a top constraint to the superview for A. Then a vertical spacing constraint between A and B. This is all normal so far. If you were to remove A at this point, B would have ambiguous layout. If you were to hide it, it would still occupy it's space including the space between the labels.
Next you need to add another constraint from B, to the top of the superview. Change the priority on this to be lower than the others (say 900) and then set it's constant to be standard (or other smaller value). Now, when A is removed from it's superview, the lower priority constraint will kick in and pull B towards the top. The constraints look something like this:
The issue comes when you try to do this with a long list of labels.
I found a seemlingly decent way to do this as well. It's similar to David's. Here's how it works in code. I created the superview and all it's subviews, even the ones that may not always be showing. I added many of the constraints such as V:|-[_btn]
to the superview. As you can see at the end of those constraints there is no link to the bottom on the superview. I then created two arrays of constraints for both states of the view, for me the difference is a 'More Options' disclosure triangle. Then when the triangle is clicked depending on it's state I add and remove constraints and subviews accordingly. For example to add I do:
[self.backgroundView removeConstraints:self.lessOptionsConstraints];
[self.backgroundView addSubview:self.nameField];
[self.backgroundView addConstraints:self.moreOptionsConstraints];
The constraints I removed tied the button to the bottom of the superview like V:[_btn]-|
. The constraints I added look like V:[_btn]-[_nameField]-|
as you can see this constraint places the new view in between the original view above it and the bottom of the superview which extends the superview's height.