How to dynamically controls constraint to active them without manually doing calculation and set active by my self?

96 views Asked by At

I want the text label adjust its position based on the green view. It should left alignment to the view if possible, but if the text content extent out of the dashboard, I'd like the text to right alignment to the view. Example:

example

The green view's position and changed randomly, I don't want to calculate the text frame every time and manually enable or disable different constraints, or set frame directly. I really want to explore some easy way to achieve that

Currently I uses:

C1 = [text.leadingAnchor constraintEqualToAnchor:view.leadingAnchor]
C2 = [text.trailingAnchor constraintLessThanOrEqualToAnchor:view.trailingAnchor]
C3 = [text.trailingAnchor constraintLessThanOrEqualToAnchor:dashboard.trailingAnchor]

priority: C3>C2>C1

But in that case, the text position for the middle green view is wrong.

2

There are 2 answers

0
Rob On

Your question, effectively, whether you could have it right aligned with the green view if it would fit, and otherwise left align it. There is no simple set of constraints that will be able to do that automatically. You could theoretically have two constraints, one for left alignment and another for right alignment, and then programmatically activate one and deactivate the other based upon where the green view was within its superview. (Or, needless to say, you could go completely old-school, and adjust frames manually.)

But there is a simple alternative that keeps it entirely within the scope of the constraints system, without any manual adjustments, but it isn’t precisely what you asked for. But, it might be sufficient for your purposes: Specifically, you could right align it with the green view if there is space, and left align it with the green view’s superview if not.

The basic idea is to have required constraints for the label with respect to the container (your dashboard view), but a lower priority constraint with respect to the green view’s trailing edge.

You might also want to set the priorities of the label’s content hugging and content compression resistance. These priorities should be higher than the green view’s trailing anchor constraint. E.g.:

let c1 = label.leadingAnchor.constraint(greaterThanOrEqualTo: dashboard.leadingAnchor)
let c2 = label.trailingAnchor.constraint(equalTo: greenView.trailingAnchor)
let c3 = label.trailingAnchor.constraint(lessThanOrEqualTo: dashboard.trailingAnchor)

c2.priority = .defaultLow

label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
label.setContentHuggingPriority(.required, for: .horizontal)

NSLayoutConstraint.activate([c1, c2, c3])

Yields:

enter image description here


Or, in Objective-C:

NSLayoutConstraint *c1 = [label.leadingAnchor constraintGreaterThanOrEqualToAnchor:dashboard.leadingAnchor];
NSLayoutConstraint *c2 = [label.trailingAnchor constraintEqualToAnchor:greenView.trailingAnchor];
NSLayoutConstraint *c3 = [label.trailingAnchor constraintLessThanOrEqualToAnchor:dashboard.trailingAnchor];

c2.priority = UILayoutPriorityDefaultLow;

[label setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal];
[label setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];

[NSLayoutConstraint activateConstraints:@[c1, c2, c3]];

Or, needless to say, you can set all of these constraints and priorities in Interface Builder, not requiring any code at all. But hopefully this illustrates how to use priorities to achieve the desired effect.

0
DonMag On

As mentioned, this cannot be accomplished with constraints only, but the logic is fairly straightforward and we can take advantage of constraints for most of the work.

So, the goal:

  • align the label leading to the "green view" leading
  • IF the label extends past the trailing edge of the superview,
    • align the label trailing to the "green view" trailing

Obviously, if the string is shorter than the view width, we don't have to do anything.

enter image description here

When the label is wider than the view:

  • create both Leading and Trailing constraints
  • toggle the .active property of the constraints as needed

enter image description here

But -- what should happen if the label is too wide to fit when it is right-aligned?

enter image description here

We can lower the priority of the leading/trailing constraints from the label to the "green view" and add required constraints between the label and the superview:

enter image description here

This may end up looking a little "un-balanced" -- so we can add in one more piece of logic... we'll use the wider "gap" on the left/right of the "green view" and the superview to give priority to the layout:

enter image description here

Here is some sample code...


AttachedLabelView.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN
@interface AttachedLabelView : UIView
@property (strong, nonatomic) UILabel *attachedLabel;
@end
NS_ASSUME_NONNULL_END

AttachedLabelView.m

#import "AttachedLabelView.h"

@interface AttachedLabelView ()
{
    UILabel *myLabel;
    NSLayoutConstraint *labelLeading;
    NSLayoutConstraint *labelTrailing;
    NSLayoutConstraint *limitLabelLeading;
    NSLayoutConstraint *limitLabelTrailing;
}

@end

@implementation AttachedLabelView

- (void)setAttachedLabel:(UILabel *)attachedLabel {

    // make sure this is set
    attachedLabel.translatesAutoresizingMaskIntoConstraints = NO;
    
    // initialize leading and trailing constraints
    //  these will be toggled so the label is either
    //  aligned with the leading edge, or
    //  aligned with the trailing edge
    labelLeading = [attachedLabel.leadingAnchor constraintEqualToAnchor:self.leadingAnchor];
    labelTrailing = [attachedLabel.trailingAnchor constraintEqualToAnchor:self.trailingAnchor];
    labelLeading.priority = UILayoutPriorityDefaultHigh;
    labelTrailing.priority = UILayoutPriorityDefaultHigh;

    // label will always be "pinned" to the bottom of self
    [attachedLabel.topAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES;
    
    myLabel = attachedLabel;
    
    [self setNeedsLayout];
}
- (UILabel *)attachedLabel {
    return myLabel;
}
- (void)layoutSubviews {
    [super layoutSubviews];
    
    UIView *sv = self.superview;
    UILabel *myLabel = self.attachedLabel;
    
    // don't try to do anything if we don't have
    //  a superview AND an "attached" label
    if (!sv || !myLabel) {
        return;
    }
    
    // if not yet set, do so now
    //  these constraints prevent the label from extending outside the superview frame
    if (!limitLabelLeading) {
        limitLabelLeading = [myLabel.leadingAnchor constraintGreaterThanOrEqualToAnchor:sv.leadingAnchor];
        limitLabelLeading.active = YES;
        limitLabelTrailing = [myLabel.trailingAnchor constraintLessThanOrEqualToAnchor:sv.trailingAnchor];
        limitLabelTrailing.active = YES;
    }
    
    // disable both constraints
    labelLeading.active = NO;
    labelTrailing.active = NO;
    
    // get self's "left" and "right"
    CGFloat myMinX = CGRectGetMinX(self.frame);
    CGFloat myMaxX = CGRectGetMaxX(self.frame);
    
    // get the widths of the
    //  leading "empty space" and
    //  trailing "empty space"
    CGFloat leadingGap = myMinX;
    CGFloat trailingGap = sv.frame.size.width - myMaxX;
    
    // the logic:
    //  IF the label is too long to fit in the superview when "left-aligned"
    // AND
    //  IF there is more space on "my left" than on "my right"
    // then we use the Trailing constraint
    
    if (myMinX + myLabel.intrinsicContentSize.width > sv.frame.size.width) {
        if (leadingGap > trailingGap) {
            labelTrailing.active = YES;
        }
    }
    
    labelLeading.active = !labelTrailing.active;
}

@end

SimpleExampleViewController.h - simple example showing usage...

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN
@interface SimpleExampleViewController : UIViewController
@end
NS_ASSUME_NONNULL_END

SimpleExampleViewController.m

#import "SimpleExampleViewController.h"
#import "AttachedLabelView.h"

@interface SimpleExampleViewController ()
{
    UIView *dashboardView;
    AttachedLabelView *exampleView;
    UILabel *someLabel;
}
@end

@implementation SimpleExampleViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // change the string and the x-position to see the differences
    NSString *sample = @"This string is a little longer.";
    CGFloat xPos = 40.0;
    
    self.view.backgroundColor = UIColor.systemBackgroundColor;
    
    dashboardView = [UIView new];
    dashboardView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0];
    
    exampleView = [AttachedLabelView new];
    exampleView.backgroundColor = UIColor.systemGreenColor;
    
    someLabel = [UILabel new];
    someLabel.backgroundColor = UIColor.cyanColor;
    
    dashboardView.translatesAutoresizingMaskIntoConstraints = NO;
    exampleView.translatesAutoresizingMaskIntoConstraints = NO;
    someLabel.translatesAutoresizingMaskIntoConstraints = NO;
    
    // add the custom view and label to the dashboardView
    [dashboardView addSubview:exampleView];
    [dashboardView addSubview:someLabel];
    
    // add dashboardView
    [self.view addSubview:dashboardView];
    
    UILayoutGuide *g = self.view.safeAreaLayoutGuide;
    
    [NSLayoutConstraint activateConstraints:@[

        [dashboardView.topAnchor constraintEqualToAnchor:g.topAnchor constant:20.0],
        [dashboardView.heightAnchor constraintEqualToConstant:140.0],
        [dashboardView.widthAnchor constraintEqualToConstant:340.0],
        [dashboardView.centerXAnchor constraintEqualToAnchor:g.centerXAnchor],

        [exampleView.widthAnchor constraintEqualToConstant:120.0],
        [exampleView.heightAnchor constraintEqualToConstant:80.0],
        [exampleView.topAnchor constraintEqualToAnchor:dashboardView.topAnchor constant:20.0],
        [exampleView.leadingAnchor constraintEqualToAnchor:dashboardView.leadingAnchor constant:xPos],

    ]];

    someLabel.text = sample;

    // "attach" the label to exampleView
    [exampleView setAttachedLabel:someLabel];

}

@end

AttachExampleViewController.h - a more complex example, showing usage with "draggable" green view and "tap to cycle" through sample labels...

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN
@interface AttachExampleViewController : UIViewController
@end
NS_ASSUME_NONNULL_END

AttachExampleViewController.m

#import "AttachExampleViewController.h"
#import "AttachedLabelView.h"

@interface AttachExampleViewController ()
{
    UIView *dashboardView;
    AttachedLabelView *exampleView;
    UILabel *someLabel;
    NSMutableArray <NSString *>*samples;
    NSLayoutConstraint *evTopConstraint;
    NSLayoutConstraint *evLeadingConstraint;
}
@end

@implementation AttachExampleViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // some sample strings to cycle through
    samples = [NSMutableArray arrayWithObjects:
               @"Sample string.",
               @"This string is a little longer.",
               @"Might extend outside the superview.",
               nil
    ];
    
    self.view.backgroundColor = UIColor.systemBackgroundColor;
    
    dashboardView = [UIView new];
    dashboardView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0];
    
    exampleView = [AttachedLabelView new];
    exampleView.backgroundColor = UIColor.systemGreenColor;
    
    someLabel = [UILabel new];
    someLabel.backgroundColor = UIColor.cyanColor;
    
    dashboardView.translatesAutoresizingMaskIntoConstraints = NO;
    exampleView.translatesAutoresizingMaskIntoConstraints = NO;
    someLabel.translatesAutoresizingMaskIntoConstraints = NO;

    // add the custom view and label to the dashboardView
    [dashboardView addSubview:exampleView];
    [dashboardView addSubview:someLabel];
    
    // add dashboardView
    [self.view addSubview:dashboardView];

    UILayoutGuide *g = self.view.safeAreaLayoutGuide;
    
    [NSLayoutConstraint activateConstraints:@[
        [dashboardView.topAnchor constraintEqualToAnchor:g.topAnchor constant:20.0],
        [dashboardView.heightAnchor constraintEqualToConstant:140.0],
        [dashboardView.widthAnchor constraintEqualToConstant:340.0],
        [dashboardView.centerXAnchor constraintEqualToAnchor:g.centerXAnchor],
    ]];
    
    // add a "prompt" label
    UILabel *pLabel = [UILabel new];
    pLabel.numberOfLines = 0;
    pLabel.textAlignment = NSTextAlignmentCenter;
    pLabel.text = @"Tap outside the gray \"dashboard\" view\nto cycle through the sample strings.";
    pLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:pLabel];

    [NSLayoutConstraint activateConstraints:@[
        [pLabel.topAnchor constraintEqualToAnchor:dashboardView.bottomAnchor constant:20.0],
        [pLabel.leadingAnchor constraintEqualToAnchor:dashboardView.leadingAnchor],
        [pLabel.trailingAnchor constraintEqualToAnchor:dashboardView.trailingAnchor],
    ]];

    // start with exampleView at 20,40
    evTopConstraint = [exampleView.topAnchor constraintEqualToAnchor:dashboardView.topAnchor constant:20.0];
    evLeadingConstraint = [exampleView.leadingAnchor constraintEqualToAnchor:dashboardView.leadingAnchor constant:40.0];
    
    // this avoids auto-layout console warnings
    evTopConstraint.priority = UILayoutPriorityRequired - 1;
    evLeadingConstraint.priority = UILayoutPriorityRequired - 1;

    [NSLayoutConstraint activateConstraints:@[
        evTopConstraint, evLeadingConstraint,
        [exampleView.widthAnchor constraintEqualToConstant:120.0],
        [exampleView.heightAnchor constraintEqualToConstant:80.0],
    ]];
    
    // we're making exampleView draggable
    UIPanGestureRecognizer *p = [UIPanGestureRecognizer new];
    [p addTarget:self action:@selector(handlePan:)];
    [exampleView addGestureRecognizer:p];
    
    // "attach" the label to exampleView
    [exampleView setAttachedLabel:someLabel];
    
    // update the label text
    [self nextString];
}

- (void)handlePan:(UIPanGestureRecognizer *)p {

    UIView *v = p.view;
    UIView *sv = v.superview;
    CGPoint pt = [p translationInView:v];
    [p setTranslation:CGPointZero inView:v];
    
    // let's prevent dragging the exampleView outside of the dashboardView frame
    CGFloat xMax = sv.frame.size.width - v.frame.size.width;
    CGFloat yMax = sv.frame.size.height - (v.frame.size.height + someLabel.frame.size.height);

    CGFloat x = evLeadingConstraint.constant + pt.x;
    CGFloat y = evTopConstraint.constant + pt.y;

    evLeadingConstraint.constant = MIN(xMax, MAX(x, 0.0));
    evTopConstraint.constant = MIN(yMax, MAX(y, 0.0));

    // tell exampleView to update the label position (if necessary)
    [exampleView setNeedsLayout];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    UITouch *t = [touches anyObject];
    CGPoint p = [t locationInView:self.view];
    if (CGRectContainsPoint(dashboardView.frame, p)) {
        return;
    }
    // if we tap outside of dashboardView, cycle to the next sample string
    [self nextString];
}
- (void)nextString {
    NSString *s = [samples firstObject];
    [samples removeObject:s];
    [samples addObject:s];
    someLabel.text = s;

    // tell exampleView to update the label position (if necessary)
    [exampleView setNeedsLayout];
}

@end