简体   繁体   English

带有Slider,textField和Auto布局的UIStackView

[英]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. 但是我不确定是否可以在stackView中添加约束,如果我错了,请纠正我。

Vertical stackView have Leading alignment, Horizontal - Center alignment, here is build result 垂直stackView具有前导对齐,水平-居中对齐,这是生成结果 在此处输入图片说明

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 :( 我想将剩下的所有内容对齐,但是我不知道如何,我可以使用IB来做到这一点,但是相同的设置在编程上不起作用:(

PS Also sometimes I'm getting this in log: PS也有时我在日志中得到这个:

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. UIStackView很棒,除非不是。

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. 要使用您采用的方法-具有“行”水平堆栈视图的Vertical UIStackView您需要在每个元素上明确设置宽度约束。

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; 因此,您要么需要提前确定“ Description Label的宽度为80,要么确定其宽度。 Val-1 has a width of 40; Val-1的宽度为40; Slider has a width of 150; 滑块的宽度为150; Val-2 has a width of 80; Val-2的宽度为80; and Input has a width of 100. Input的宽度为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: 首先,我做了一个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

Then I made a ParameterView class with this interface: 然后,我使用此接口制作了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

Notice that ParameterView is a subclass of UIStackView . 注意, ParameterViewUIStackView的子类。 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. 请注意,在找到可以显示最小值和最大值而无需裁剪的最小宽度之后,我在_valueTextField.widthAnchor上设置了大于或等于约束。

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`: 所以我在视图控制器的'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. 这是我完整的ViewController.m文件,以备您使用。 Just copy and paste it into a newly-created iOS project. 只需将其复制并粘贴到新创建的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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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