iOS自动布局:垂直等间距填充父视图

14

我有一个视图控制器,包括12个UITextField

它非常适合3.5英寸的显示屏。

enter image description here

我需要将其设置为适用于iPhone 5(4英寸显示屏),以使所有UITextField在它们之间添加额外的空间并覆盖整个UIView

我正在尝试通过自动布局来实现此目标,但是它无法正常工作。

enter image description here

这是我的代码:

- (void) viewWillLayoutSubviews
{
    int h = txt1.bounds.size.height * 12;

    float unusedHorizontalSpace = self.view.bounds.size.height - h ;

    NSNumber* spaceBetweenEachButton=  [NSNumber numberWithFloat: unusedHorizontalSpace / 13 ] ;

    NSMutableArray *constraintsForButtons = [[NSMutableArray alloc] init];

    [constraintsForButtons addObjectsFromArray: [NSLayoutConstraint constraintsWithVisualFormat: @"V:|-50-[txt1(==30)]-(space)-[txt2(==txt1)]-(space)-[txt3(==txt1)]-(space)-[txt4(==txt1)]-(space)-[txt5(==txt1)]-(space)-[txt6(==txt1)]-(space)-[txt7(==txt1)]-(space)-[txt8(==txt1)]-(space)-[txt9(==txt1)]-(space)-[txt10(==txt1)]-(space)-[txt11(==txt1)]-(space)-[txt12]-(space)-|"
                                                                                        options: NSLayoutFormatAlignAllCenterX
                                                                                        metrics: @{@"space":spaceBetweenEachButton}
                                                                                          views: NSDictionaryOfVariableBindings(txt1,txt10,txt11,txt12,txt2,txt3,txt4,txt5,txt6, txt7,txt8,txt9)]];

    [self.view addConstraints:constraintsForButtons];
}

如果我执行 [txt12(==txt1)],它会显示与3.5英寸屏幕相同的内容,并在下方留出空间。

我错在哪里?


在你的 constraintsWithVisualFormat 字符串中,你的第一个 "space" 是一个常量 50,而你只使用了 12 个 "space" 标识符,即使你计算了 13 个。要么将 50 更改为 (space),要么将 spaceBetweenEachButton 计算为 unusedHorizontalSpace/12 - 50。不确定是否有效,但值得一试。 - Joel H.
上面的代码实际上给你什么? - Hari Honor
请查看问题中的第二张图片。 - CRDave
哦,我明白了。你的间距是计算出来然后作为一个固定值传递的,对吗?你确定这个值在Retina4尺寸下被正确计算得更大了吗? - Hari Honor
1
我有一个不使用间隔符并使用IB的好解决方案。这里 - SmileBot
3个回答

49
要使用自动布局实现这一点,您必须创建额外的视图来填充文本字段之间的空白。
回想一下,自动布局约束基本上是线性方程A = m * B + c。A是一个视图的属性(例如,viewA底部边缘的Y坐标),B是另一个视图的属性(例如,viewB顶部边缘的Y坐标)。m和c是常数。因此,例如,为了布局viewA和viewB,使得viewA的底部和viewB的顶部之间有30个像素,我们可以创建一个约束,其中m为1,c为-30。
您遇到的问题是希望在13个不同的约束条件中使用相同的 c 值,并且希望自动布局为您计算 c 值。但是,自动布局无法直接完成这项任务。自动布局只能计算视图的属性;它无法计算 m 和 c 常量。
有一种方法可以使自动布局将视图放置在所需位置:将文本字段之间的空间重新定义为其他(不可见)视图。以下是仅包含3个文本字段的示例:

example layout

我们将创建一个约束来将每个间隔的顶部边缘固定在其上方文本字段的底部边缘上。我们还将创建一个约束,将每个间隔的底部边缘固定在其下方文本字段的顶部边缘上。最后,我们将创建一个约束,强制每个间隔具有与最上面的间隔相同的高度。
我们需要两个实例变量来设置:一个文本字段数组(按从上到下的顺序),以及对最上面的间隔视图的引用。
@implementation ViewController {
    NSMutableArray *textFields;
    UIView *topSpacer;
}

由于在stackoverflow答案中难以展示xib,因此我们将在代码中创建文本字段和间距。我们将从viewDidLoad开始:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.translatesAutoresizingMaskIntoConstraints = NO;
    [self addTextFields];
    [self addSpacers];
}

由于我们将使用自动布局,因此需要关闭translatesAutoresizingMaskIntoConstraints以防止系统创建额外的约束。

我们创建每个文本字段,为其提供一些虚拟文本,并设置其水平位置和大小的约束:

- (void)addTextFields {
    textFields = [NSMutableArray array];
    for (int i = 0; i < 12; ++i) {
        [self addTextField];
    }
}

- (void)addTextField {
    UITextField *field = [[UITextField alloc] init];
    field.backgroundColor = [UIColor colorWithHue:0.8 saturation:0.1 brightness:0.9 alpha:1];
    field.translatesAutoresizingMaskIntoConstraints = NO;
    field.text = [field description];
    [self.view addSubview:field];
    [field setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
    [field setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[field]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(field)]];
    [textFields addObject:field];
}

我们将使用循环来创建间隔符,但是我们会以不同的方式创建顶部和底部的间隔符,因为我们需要将它们固定在父视图上:
- (void)addSpacers {
    [self addTopSpacer];
    for (int i = 1, count = textFields.count; i < count; ++i) {
        [self addSpacerFromBottomOfView:textFields[i - 1]
            toTopOfView:textFields[i]];
    }
    [self addBottomSpacer];
}

这是我们创建顶部间距并设置其约束的方法。它的顶部边缘固定在父视图上,底部边缘固定在第一个(最上面的)文本字段上。我们将顶部间距存储在实例变量topSpacer中,以便将其他间距约束为与顶部间距相同的高度。
- (void)addTopSpacer {
    UIView *spacer = [self newSpacer];
    UITextField *field = textFields[0];
    [self.view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"V:|[spacer][field]" options:0 metrics:nil
        views:NSDictionaryOfVariableBindings(spacer, field)]];
    topSpacer = spacer;
}

这里是我们实际创建间隔视图的方法。它只是一个隐藏的视图。由于我们不关心其水平大小或位置,因此我们只需将其固定在父视图的左右边缘即可。
- (UIView *)newSpacer {
    UIView *spacer = [[UIView alloc] init];
    spacer.hidden = YES; // Views participate in layout even when hidden.
    spacer.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:spacer];
    [self.view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"|[spacer]|" options:0 metrics:nil
        views:NSDictionaryOfVariableBindings(spacer)]];
    return spacer;
}

要在两个文本视图之间创建一个“中间”间隔,我们将其固定在上面的文本字段的底部边缘和下面的文本字段的顶部边缘。我们还将其高度限制为等于顶部间隔的高度。

- (void)addSpacerFromBottomOfView:(UIView *)overView toTopOfView:(UIView *)underView {
    UIView *spacer = [self newSpacer];
    [self.view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"V:[overView][spacer(==topSpacer)][underView]" options:0 metrics:nil
        views:NSDictionaryOfVariableBindings(spacer, overView, underView, topSpacer)]];
}

为了创建底部间隔,我们将其固定在最后一个文本字段和父视图上。我们还将其高度约束为等于顶部间隔的高度。
- (void)addBottomSpacer {
    UIView *spacer = [self newSpacer];
    UITextField *field = textFields.lastObject;
    [self.view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"V:[field][spacer(==topSpacer)]|" options:0 metrics:nil
        views:NSDictionaryOfVariableBindings(spacer, field, topSpacer)]];
}

如果你做得正确,你会得到这样的结果:

example code screen shot

您可以在此 GitHub 存储库中找到一个完整的示例项目。


我感到困惑:“你想让自动布局计算那个c值给你。自动布局根本无法做到这一点。” 如果“c”是间距,那么他在代码中明确指定了它... - Hari Honor
他不想指定它。他希望自动布局计算它,这样他就不必自己计算了。 - rob mayoff
1
很遗憾,这个答案是错误的。你可以在不使用间隔视图的情况下实现所需的效果。请查看我创建的UIView+AutoLayout类别中autoDistributeSubviews:alongAxis:withFixedSize:alignment:方法的实现中解决此问题的通用解决方案。 - smileyborg
4
我的答案不需要固定的文本框大小。如果用户可以更改字体,或者应用程序在iOS 7下支持动态类型,则我的答案将继续正确地布局视图,而无需任何其他代码。即使每个文本框的大小不同,我的答案也能正常工作。 - rob mayoff
@rob mayoff 很棒的答案。指引了我正确的方向。我通过仅使用IB / Storyboard来解决问题,但依赖于您所描述的概念。 - marco alves
显示剩余6条评论

10

请查看PureLayout。它被设计成最简单且最适合程序员的API,用于在代码中创建自动布局约束。

针对您特定的问题,PureLayout提供了两个主要的API用于分配视图,一个是间距固定(视图大小根据需要变化),另一个是视图大小固定(视图间距根据需要变化)。后者将实现您正在寻找的内容而无需使用任何“ spacer views”。

// NSArray+PureLayout.h

// ...

/** Distributes the views in this array equally along the selected axis in their superview. Views will be the same size (variable) in the dimension along the axis and will have spacing (fixed) between them. */
- (NSArray *)autoDistributeViewsAlongAxis:(ALAxis)axis
                                alignedTo:(ALAttribute)alignment
                         withFixedSpacing:(CGFloat)spacing;

/** Distributes the views in this array equally along the selected axis in their superview. Views will be the same size (fixed) in the dimension along the axis and will have spacing (variable) between them. */
- (NSArray *)autoDistributeViewsAlongAxis:(ALAxis)axis
                                alignedTo:(ALAttribute)alignment
                            withFixedSize:(CGFloat)size;

// ...

我会尝试一下。看起来你做得很好。试着把它放在www.cocoacontrols.com和cocoapods.org上,这样更多的人就可以看到它了。 - CRDave
2
谢谢!我一定会这样做。如果您有任何问题、反馈、建议或发现错误,请随时告诉我! - smileyborg

2

是的,我知道...苹果现在已经更新了那个文档。现在为了实现等间距,他们引入了UIStackview。因此,现在不需要再放置spacerViews(在链接中描述的内容)了。你现在可以轻松地使用UIStackviews来完成同样的任务。 - Mehul Thakkar
那个链接中的解释与Rob的答案几乎相同。因此,如果您想支持iOS 9之前的版本,则只能走那条路。 - Mehul Thakkar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接