UIStackView带有Slider,textField和Auto布局

问题描述 投票:0回答:2

我正在尝试以编程方式实现此布局:

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

这是垂直布局,内部有水平布局

第一个问题,滑块尺寸是多少?未知,所以我添加宽度约束:

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

但我不确定在stackView中添加约束是否可以,如果我错了请纠正我。

垂直stackView具有前导对齐,水平 - 中心对齐,这里是构建结果enter image description here

我想要对齐所有内容,但我不知道如何,我可以使用IB做到这一点,但相同的设置编程不工作:(

附:有时候我会在日志中得到这个:

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

ios autolayout uistackview
2个回答
1
投票

我不认为你想要滑块上的宽度约束。看着你的屏幕截图,我想你可能想要这个:

my layout screen shot

所以我们有一些参数。每个参数都有一个名称,一个最小值,一个最大值和一个当前值。对于每个参数,我们有一行显示所有参数的属性,当前值显示为滑块和文本字段。名称标签都具有相同的宽度,这是最窄的宽度,不会剪切任何标签。最小值和最大值标签相同。值文本字段都具有相同的宽度,并且它是最窄的宽度,即使在最小值或最大值也不会剪切值字符串。滑块根据需要展开或收缩,以填充其行中的所有剩余空间。

以下是我的编写方式。

首先,我做了一个Parameter课程:

@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

然后我用这个界面制作了一个ParameterView类:

@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

请注意,ParameterViewUIStackView的子类。初始化程序如下所示:

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;
}

请注意,在找到可以显示最小值和最大值而没有剪切的最小宽度之后,我在_valueTextField.widthAnchor上设置了大于或等于约束。

我使用辅助方法来创建每个标签值,因为它们都以相同的方式创建:

- (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],
        ]];
}

由于这会在两个不同的行中创建视图之间的约束,因此在两个行都具有公共超视图之前我无法使用它。所以我在视图控制器的'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];
        }
    }
}

这是我完整的ViewController.m文件,以防你想玩它。只需将其复制并粘贴到新创建的iOS项目中即可。

#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

2
投票

UIStackView是伟大的,除非它不是。

要使用您正在采用的方法 - 具有水平堆栈视图“行”的垂直UIStackView - 您需要在每个元素上显式设置宽度约束。

没有明确的宽度,你会得到:

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

如您所见,每行中的第一个元素 - 描述标签 - 与另一行的描述标签的宽度不同,单独的堆栈视图将根据其内在宽度排列它们。在这种情况下,文本的长度。

所以,你要么提前决定Description Label的宽度,比如80; Val-1的宽度为40;滑块的宽度为150; Val-2的宽度为80;和Input的宽度为100。

现在你将能够获得:

|Description Label   |Val-1|--slider--|Val-2| Input    |
|Short label         | 123 |--slider--| 123 | 42.6623  |
|A much longer label |  0  |--slider--|  0  | -42.6623 |
© www.soinside.com 2019 - 2024. All rights reserved.