简体   繁体   中英

UIStackView with Slider, textField and Auto layout

I'm trying programmatically implement this layout:

[label|label|slider|label|textField]
[label|label|slider|label|textField]
[label|label|slider|label|textField]

this is vertical layout with horizontal layouts inside

First problem, what will be the slider size? Unknown, so I add width constraint:

[stackView addConstraint:[NSLayoutConstraint constraintWithItem:slider
                                                       attribute:NSLayoutAttributeWidth
                                                       relatedBy:NSLayoutRelationEqual
                                                          toItem:nil
                                                       attribute:NSLayoutAttributeNotAnAttribute
                                                      multiplier:1.0
                                                        constant:150]];

but i'm not sure if it's ok to add constrain in stackView, correct me please if I'm wrong.

Vertical stackView have Leading alignment, Horizontal - Center alignment, here is build result 在此处输入图片说明

I want to align everything left, but I don't know how, I can do this using IB but the same settings programmatically not working :(

PS Also sometimes I'm getting this in log:

Failed to rebuild layout engine without detectable loss of precision. This should never happen. Performance and correctness may suffer.

UIStackView is great, except when it's not.

To use the approach you're taking - a Vertical UIStackView with "rows" of Horizontal stack views - you'll need to explicitly set a width constraint on every element.

Without explicit widths, you get:

|Description Label|Val-1|--slider--|Val-2|Input|
|Short label|123|--slider--|123|42.6623|
|A much longer label|0|--slider--|0|-42.6623|

As you can see, the first element in each row - the Description Label - does not have an equal width to the other row's Description Labels, and the separate stack views will arrange them based on their intrisic widths. In this case, the length of the text.

So, you either need to decide ahead of time that Description Label has a width of, say, 80; Val-1 has a width of 40; Slider has a width of 150; Val-2 has a width of 80; and Input has a width of 100.

Now you'll be able to get:

|Description Label   |Val-1|--slider--|Val-2| Input    |
|Short label         | 123 |--slider--| 123 | 42.6623  |
|A much longer label |  0  |--slider--|  0  | -42.6623 |

I don't think you want a width constraint on the slider. Looking at your screen shot, I guess you probably want this:

我的布局屏幕截图

So we have some parameters. Each parameter has a name, a minimum value, a maximum value, and a current value. For each parameter, we have one row showing all of the parameter's properties, with the current value shown as both a slider and a text field. The name labels all have the same width, which is the narrowest width that doesn't clip any of the labels. Same for the minimum and maximum value labels. The value text fields all have the same width, and it is the narrowest width that won't clip the value string even at the minimum or maximum value. The slider expands or contracts as needed to fill all remaining space in its row.

Here's how I did it, all in code.

First, I made a Parameter class:

@interface Parameter: NSObject
@property (nonatomic, copy, readonly, nonnull) NSString *name;
@property (nonatomic, readonly) double minValue;
@property (nonatomic, readonly) double maxValue;
@property (nonatomic) double value;

- (instancetype _Nonnull)initWithName:(NSString *_Nonnull)name minValue:(double)minValue maxValue:(double)maxValue initialValue:(double)value;
@end

@implementation Parameter
- (instancetype)initWithName:(NSString *)name minValue:(double)minValue maxValue:(double)maxValue initialValue:(double)value {
    if (self = [super init]) {
        _name = [name copy];
        _minValue = minValue;
        _maxValue = maxValue;
        _value = value;
    }
    return self;
}
@end

Then I made a ParameterView class with this interface:

@interface ParameterView: UIStackView
@property (nonatomic, strong, readonly, nonnull) Parameter *parameter;

@property (nonatomic, strong, readonly, nonnull) UILabel *nameLabel;
@property (nonatomic, strong, readonly, nonnull) UILabel *minValueLabel;
@property (nonatomic, strong, readonly, nonnull) UISlider *valueSlider;
@property (nonatomic, strong, readonly, nonnull) UILabel *maxValueLabel;
@property (nonatomic, strong, readonly, nonnull) UITextField *valueTextField;

- (instancetype _Nonnull)initWithParameter:(Parameter *_Nonnull)parameter;
@end

Notice that ParameterView is a subclass of UIStackView . The initializer looks like this:

static void *kvoParameterValue = &kvoParameterValue;

@implementation ParameterView

- (instancetype)initWithParameter:(Parameter *)parameter {

    if (self = [super init]) {
        self.axis = UILayoutConstraintAxisHorizontal;
        self.alignment = UIStackViewAlignmentFirstBaseline;
        self.spacing = 2;

        _parameter = parameter;

        _nameLabel = [self pv_labelWithText:[parameter.name stringByAppendingString:@":"] alignment:NSTextAlignmentRight];
        _minValueLabel = [self pv_labelWithText:[NSString stringWithFormat:@"%.0f", parameter.minValue] alignment:NSTextAlignmentRight];
        _maxValueLabel = [self pv_labelWithText:[NSString stringWithFormat:@"%.0f", parameter.maxValue] alignment:NSTextAlignmentLeft];

        _valueSlider = [[UISlider alloc] init];
        _valueSlider.translatesAutoresizingMaskIntoConstraints = NO;
        _valueSlider.minimumValue = parameter.minValue;
        _valueSlider.maximumValue = parameter.maxValue;

        _valueTextField = [[UITextField alloc] init];
        _valueTextField.translatesAutoresizingMaskIntoConstraints = NO;
        _valueTextField.borderStyle = UITextBorderStyleRoundedRect;
        _valueTextField.text = [self stringWithValue:parameter.minValue];
        CGFloat width = [_valueTextField systemLayoutSizeFittingSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)].width;
        _valueTextField.text = [self stringWithValue:parameter.maxValue];
        width = MAX(width, [_valueTextField systemLayoutSizeFittingSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)].width);
        [_valueTextField.widthAnchor constraintGreaterThanOrEqualToConstant:width].active = YES;

        [self addArrangedSubview:_nameLabel];
        [self addArrangedSubview:_minValueLabel];
        [self addArrangedSubview:_valueSlider];
        [self addArrangedSubview:_maxValueLabel];
        [self addArrangedSubview:_valueTextField];

        [_parameter addObserver:self forKeyPath:@"value" options:0 context:kvoParameterValue];
        [_valueSlider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged];
        [self updateViews];
    }
    return self;
}

Notice that I set a greater-than-or-equal-to constraint on _valueTextField.widthAnchor , after finding the minimum width that can show both the minimum and maximum values without clipping.

I use a helper method to create each of the label values, since they are all created the same way:

- (UILabel *)pv_labelWithText:(NSString *)text alignment:(NSTextAlignment)alignment {
    UILabel *label = [[UILabel alloc] init];
    label.translatesAutoresizingMaskIntoConstraints = NO;
    label.text = text;
    label.textAlignment = alignment;
    [label setContentCompressionResistancePriority:UILayoutPriorityRequired - 1 forAxis:UILayoutConstraintAxisHorizontal];
    [label setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal];
    return label;
}

What I'm doing here is setting the horizontal content compression resistance priority very high, so that none of the labels will clip its content. But then I'm setting the horizontal hugging priority fairly high (but not as high) so that each label will try to be as narrow as possible (without clipping).

To make the columns of labels line up, I need one more method:

- (void)constrainColumnsToReferenceView:(ParameterView *)referenceView {
    [NSLayoutConstraint activateConstraints:@[
        [_nameLabel.widthAnchor constraintEqualToAnchor:referenceView.nameLabel.widthAnchor],
        [_minValueLabel.widthAnchor constraintEqualToAnchor:referenceView.minValueLabel.widthAnchor],
        [_valueSlider.widthAnchor constraintEqualToAnchor:referenceView.valueSlider.widthAnchor],
        [_maxValueLabel.widthAnchor constraintEqualToAnchor:referenceView.maxValueLabel.widthAnchor],
        [_valueTextField.widthAnchor constraintEqualToAnchor:referenceView.valueTextField.widthAnchor],
        ]];
}

Since this creates constraints between the views in two different rows, I can't use it until both rows have a common superview. So I use it in my view controller's 'viewDidLoad`:

- (void)viewDidLoad {
    [super viewDidLoad];

    UIStackView *rootStack = [[UIStackView alloc] init];
    rootStack.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:rootStack];
    [NSLayoutConstraint activateConstraints:@[
        [rootStack.leadingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor constant:8],
        [rootStack.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:8],
        [rootStack.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor constant:-8],
        ]];
    rootStack.axis = UILayoutConstraintAxisVertical;
    rootStack.spacing = 2;
    rootStack.alignment = UIStackViewAlignmentFill;

    ParameterView *firstParameterView;
    for (Parameter *p in _parameters) {
        ParameterView *pv = [[ParameterView alloc] initWithParameter:p];
        [rootStack addArrangedSubview:pv];
        if (firstParameterView == nil) {
            firstParameterView = pv;
        } else {
            [pv constrainColumnsToReferenceView:firstParameterView];
        }
    }
}

And here is my complete ViewController.m file, in case you want to play with it. Just copy and paste it into a newly-created iOS project.

#import "ViewController.h"

@interface Parameter: NSObject
@property (nonatomic, copy, readonly, nonnull) NSString *name;
@property (nonatomic, readonly) double minValue;
@property (nonatomic, readonly) double maxValue;
@property (nonatomic) double value;

- (instancetype _Nonnull)initWithName:(NSString *_Nonnull)name minValue:(double)minValue maxValue:(double)maxValue initialValue:(double)value;
@end

@implementation Parameter
- (instancetype)initWithName:(NSString *)name minValue:(double)minValue maxValue:(double)maxValue initialValue:(double)value {
    if (self = [super init]) {
        _name = [name copy];
        _minValue = minValue;
        _maxValue = maxValue;
        _value = value;
    }
    return self;
}
@end

@interface ParameterView: UIStackView
@property (nonatomic, strong, readonly, nonnull) Parameter *parameter;

@property (nonatomic, strong, readonly, nonnull) UILabel *nameLabel;
@property (nonatomic, strong, readonly, nonnull) UILabel *minValueLabel;
@property (nonatomic, strong, readonly, nonnull) UISlider *valueSlider;
@property (nonatomic, strong, readonly, nonnull) UILabel *maxValueLabel;
@property (nonatomic, strong, readonly, nonnull) UITextField *valueTextField;

- (instancetype _Nonnull)initWithParameter:(Parameter *_Nonnull)parameter;
@end

static void *kvoParameterValue = &kvoParameterValue;

@implementation ParameterView

- (instancetype)initWithParameter:(Parameter *)parameter {

    if (self = [super init]) {
        self.axis = UILayoutConstraintAxisHorizontal;
        self.alignment = UIStackViewAlignmentCenter;
        self.spacing = 2;

        _parameter = parameter;

        _nameLabel = [self pv_labelWithText:[parameter.name stringByAppendingString:@":"] alignment:NSTextAlignmentRight];
        _minValueLabel = [self pv_labelWithText:[NSString stringWithFormat:@"%.0f", parameter.minValue] alignment:NSTextAlignmentRight];
        _maxValueLabel = [self pv_labelWithText:[NSString stringWithFormat:@"%.0f", parameter.maxValue] alignment:NSTextAlignmentLeft];

        _valueSlider = [[UISlider alloc] init];
        _valueSlider.translatesAutoresizingMaskIntoConstraints = NO;
        _valueSlider.minimumValue = parameter.minValue;
        _valueSlider.maximumValue = parameter.maxValue;

        _valueTextField = [[UITextField alloc] init];
        _valueTextField.translatesAutoresizingMaskIntoConstraints = NO;
        _valueTextField.borderStyle = UITextBorderStyleRoundedRect;
        _valueTextField.text = [self stringWithValue:parameter.minValue];
        CGFloat width = [_valueTextField systemLayoutSizeFittingSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)].width;
        _valueTextField.text = [self stringWithValue:parameter.maxValue];
        width = MAX(width, [_valueTextField systemLayoutSizeFittingSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)].width);
        [_valueTextField.widthAnchor constraintGreaterThanOrEqualToConstant:width].active = YES;

        [self addArrangedSubview:_nameLabel];
        [self addArrangedSubview:_minValueLabel];
        [self addArrangedSubview:_valueSlider];
        [self addArrangedSubview:_maxValueLabel];
        [self addArrangedSubview:_valueTextField];

        [_parameter addObserver:self forKeyPath:@"value" options:0 context:kvoParameterValue];
        [_valueSlider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged];
        [self updateViews];
    }
    return self;
}

- (UILabel *)pv_labelWithText:(NSString *)text alignment:(NSTextAlignment)alignment {
    UILabel *label = [[UILabel alloc] init];
    label.translatesAutoresizingMaskIntoConstraints = NO;
    label.text = text;
    label.textAlignment = alignment;
    [label setContentCompressionResistancePriority:UILayoutPriorityRequired - 1 forAxis:UILayoutConstraintAxisHorizontal];
    [label setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal];
    return label;
}

- (void)constrainColumnsToReferenceView:(ParameterView *)referenceView {
    [NSLayoutConstraint activateConstraints:@[
        [_nameLabel.widthAnchor constraintEqualToAnchor:referenceView.nameLabel.widthAnchor],
        [_minValueLabel.widthAnchor constraintEqualToAnchor:referenceView.minValueLabel.widthAnchor],
        [_valueSlider.widthAnchor constraintEqualToAnchor:referenceView.valueSlider.widthAnchor],
        [_maxValueLabel.widthAnchor constraintEqualToAnchor:referenceView.maxValueLabel.widthAnchor],
        [_valueTextField.widthAnchor constraintEqualToAnchor:referenceView.valueTextField.widthAnchor],
        ]];
}

- (void)sliderValueChanged:(UISlider *)slider {
    _parameter.value = slider.value;
}

- (void)updateViews {
    _valueSlider.value = _parameter.value;
    _valueTextField.text = [self stringWithValue:_parameter.value];
}

- (NSString *)stringWithValue:(double)value {
    return [NSString stringWithFormat:@"%.4f", value];
}

- (void)dealloc {
    [_parameter removeObserver:self forKeyPath:@"value" context:kvoParameterValue];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == kvoParameterValue) {
        [self updateViews];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

@end

@interface ViewController ()

@end

@implementation ViewController {
    NSArray<Parameter *> *_parameters;
}

- (instancetype)initWithCoder:(NSCoder *)decoder {
    if (self = [super initWithCoder:decoder]) {
        _parameters = @[
                 [[Parameter alloc] initWithName:@"Rotation, deg" minValue:-180 maxValue:180 initialValue:61.9481],
                 [[Parameter alloc] initWithName:@"Field of view scale" minValue:0 maxValue:1 initialValue:0.7013],
                 [[Parameter alloc] initWithName:@"Fisheye lens distortion" minValue:0 maxValue:1 initialValue:0.3041],
                 [[Parameter alloc] initWithName:@"Tilt vertical" minValue:-90 maxValue:90 initialValue:42.6623],
                 [[Parameter alloc] initWithName:@"X" minValue:-1 maxValue:1 initialValue:0.6528],
                 [[Parameter alloc] initWithName:@"Y" minValue:-1 maxValue:1 initialValue:-0.3026],
                 [[Parameter alloc] initWithName:@"Z" minValue:-1 maxValue:1 initialValue:0],
                 ];
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    UIStackView *rootStack = [[UIStackView alloc] init];
    rootStack.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:rootStack];
    [NSLayoutConstraint activateConstraints:@[
        [rootStack.leadingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor constant:8],
        [rootStack.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:8],
        [rootStack.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor constant:-8],
        ]];
    rootStack.axis = UILayoutConstraintAxisVertical;
    rootStack.spacing = 4;
    rootStack.alignment = UIStackViewAlignmentFill;

    ParameterView *firstParameterView;
    for (Parameter *p in _parameters) {
        ParameterView *pv = [[ParameterView alloc] initWithParameter:p];
        [rootStack addArrangedSubview:pv];
        if (firstParameterView == nil) {
            firstParameterView = pv;
        } else {
            [pv constrainColumnsToReferenceView:firstParameterView];
        }
    }
}

@end

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM