带有 Slider、textField 和自动布局的 UIStackView
UIStackView with Slider, textField and Auto layout
我正在尝试以编程方式实现此布局:
[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中添加约束,如果我错了请指正。
Vertical stackView 有 Leading alignment,Horizontal - Center alignment,这里是构建结果
我想对齐左边的所有内容,但我不知道如何,我可以使用 IB 做到这一点,但相同的设置以编程方式不起作用:(
P.S。有时我也会在日志中得到这个:
Failed to rebuild layout engine without detectable loss of precision. This should never happen. Performance and correctness may suffer.
UIStackView
很棒,除非它不是。
要使用您正在采用的方法 - 垂直 UIStackView
和 "rows" 水平堆栈视图 - 您需要为每个元素明确设置宽度限制。
没有明确的宽度,你会得到:
|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 |
我认为您不需要滑块的宽度限制。看看你的屏幕截图,我想你可能想要这个:
所以我们有一些参数。每个参数都有一个名称、一个最小值、一个最大值和一个当前值。对于每个参数,我们用一行显示参数的所有属性,当前值显示为滑块和文本字段。名称标签都具有相同的宽度,这是不夹住任何标签的最窄宽度。最小值和最大值标签相同。值文本字段都具有相同的宽度,并且它是即使在最小值或最大值也不会裁剪值字符串的最窄宽度。滑块根据需要扩展或收缩以填充其行中所有剩余的 space。
这是我的做法,全部用代码完成。
首先,我做了一个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
然后我用这个接口做了一个ParameterView
class:
@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
请注意 ParameterView
是 UIStackView
的子 class。初始值设定项如下所示:
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
上设置了 greater-than-or-equal-to 约束条件,在找到可以同时显示最小值和最大值而不裁剪的最小宽度后。
我使用辅助方法来创建每个标签值,因为它们都是以相同的方式创建的:
- (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;
}
我在这里所做的是将水平内容抗压缩优先级设置得非常高,这样 none 的标签将剪辑其内容。但是后来我将水平拥抱优先级设置得相当高(但不是那么高),这样每个标签都会尽量变窄(不剪裁)。
要让标签的列对齐,我还需要一个方法:
- (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
文件,如果你想玩的话。只需将其复制并粘贴到 newly-created 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
我正在尝试以编程方式实现此布局:
[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中添加约束,如果我错了请指正。
Vertical stackView 有 Leading alignment,Horizontal - Center alignment,这里是构建结果
我想对齐左边的所有内容,但我不知道如何,我可以使用 IB 做到这一点,但相同的设置以编程方式不起作用:(
P.S。有时我也会在日志中得到这个:
Failed to rebuild layout engine without detectable loss of precision. This should never happen. Performance and correctness may suffer.
UIStackView
很棒,除非它不是。
要使用您正在采用的方法 - 垂直 UIStackView
和 "rows" 水平堆栈视图 - 您需要为每个元素明确设置宽度限制。
没有明确的宽度,你会得到:
|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 |
我认为您不需要滑块的宽度限制。看看你的屏幕截图,我想你可能想要这个:
所以我们有一些参数。每个参数都有一个名称、一个最小值、一个最大值和一个当前值。对于每个参数,我们用一行显示参数的所有属性,当前值显示为滑块和文本字段。名称标签都具有相同的宽度,这是不夹住任何标签的最窄宽度。最小值和最大值标签相同。值文本字段都具有相同的宽度,并且它是即使在最小值或最大值也不会裁剪值字符串的最窄宽度。滑块根据需要扩展或收缩以填充其行中所有剩余的 space。
这是我的做法,全部用代码完成。
首先,我做了一个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
然后我用这个接口做了一个ParameterView
class:
@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
请注意 ParameterView
是 UIStackView
的子 class。初始值设定项如下所示:
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
上设置了 greater-than-or-equal-to 约束条件,在找到可以同时显示最小值和最大值而不裁剪的最小宽度后。
我使用辅助方法来创建每个标签值,因为它们都是以相同的方式创建的:
- (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;
}
我在这里所做的是将水平内容抗压缩优先级设置得非常高,这样 none 的标签将剪辑其内容。但是后来我将水平拥抱优先级设置得相当高(但不是那么高),这样每个标签都会尽量变窄(不剪裁)。
要让标签的列对齐,我还需要一个方法:
- (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
文件,如果你想玩的话。只需将其复制并粘贴到 newly-created 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