OS X Cocoa Auto Layout hidden elements

后端 未结 8 1720
被撕碎了的回忆
被撕碎了的回忆 2021-01-31 17:06

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:

相关标签:
8条回答
  • 2021-01-31 17:46

    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!

    0 讨论(0)
  • 2021-01-31 17:49

    Collapsing UILabel subclass

    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
    
    0 讨论(0)
  • 2021-01-31 17:49

    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:

    • setting Hidden to YES on it

    AND

    • Disabling the 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();
        }
    }
    
    0 讨论(0)
  • 2021-01-31 17:50

    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
    }
    
    0 讨论(0)
  • 2021-01-31 17:51

    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:

    Interface Builder screenshot

    The issue comes when you try to do this with a long list of labels.

    0 讨论(0)
  • 2021-01-31 17:55

    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.

    0 讨论(0)
提交回复
热议问题