14

I have a view controller with 12 UITextFields.

It fits a 3.5" display very well.

enter image description here

I need to set it for iPhone 5 (4 inch display) such that all UITextFields cover the whole UIView by adding extra space in between.

I am trying to do this by auto layout but it is not working properly.

enter image description here

This is my code :

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

If I do [txt12(==txt1)] then it displays the same as the 3.5" screen and leaves space below.

Where I am making a mistake?

devios1
  • 33,997
  • 43
  • 149
  • 241
CRDave
  • 9,133
  • 5
  • 38
  • 58
  • In your `constraintsWithVisualFormat` string, your first "space" is a constant of 50, and you're only using 12 "space" identifiers, even though you calculated 13. Either change `50` to `(space)` or calculate `spaceBetweenEachButton` as `unusedHorizontalSpace/12 - 50`. Not sure if it will work, but it's worth a shot. – Joel H. Jun 13 '13 at 18:21
  • What are you actually getting with the code above? – Hari Honor Aug 01 '13 at 19:14
  • @Hari Karam Sing See second image in question. – CRDave Aug 02 '13 at 04:57
  • Oh I see. Your spacing is calculated and then passed as a fixed value no? Have you checked this value is correctly calculated as larger for the Retina4 size? – Hari Honor Aug 02 '13 at 10:49
  • 1
    I have a nice solution that doesn't use spacers and uses IB. [Here][1] [1]: http://stackoverflow.com/a/25898949/951349 – smileBot Sep 23 '14 at 03:49

3 Answers3

49

To do this with auto layout, you must create extra views to fill the spaces between the text fields.

Recall that an auto layout constraint is basically the linear equation A = m * B + c. A is an attribute of one view (for example, the Y coordinate of viewA's bottom edge) and B is an attribute of another view (for example, the Y coordinate of viewB's top edge). m and c are constants. So, for example, to lay out viewA and viewB so that there are 30 points between the bottom of viewA and the top of viewB, we could create a constraint where m is 1 and c is -30.

The problem you're having is that you want to use the same value for c across 13 different constraints, and you want auto layout to compute that c value for you. Auto layout simply can't do that. Not directly. Auto layout can only compute the attributes of views; it cannot compute the m and c constants.

There is a way to make auto layout put the views where you want: reify the spaces between the text fields as additional (invisible) views. Here's an example with just 3 text fields:

example layout

We'll create a constraint to pin each spacer's top edge to the bottom edge of the text field above it. We'll also create a constraint to pin each spacer's bottom edge to the top edge of the text field below it. And finally, we'll create a constraint to force each spacer to have the same height as the topmost spacer.

We'll need a two instance variables to set things up: an array of the text fields (in order from top to bottom), and a reference to the topmost spacer view:

@implementation ViewController {
    NSMutableArray *textFields;
    UIView *topSpacer;
}

We'll create the text fields and spacers in code since it's hard to show a xib in a stackoverflow answer. We kick things off in viewDidLoad:

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

Since we're going to use auto layout, we need to turn off translatesAutoresizingMaskIntoConstraints to prevent the system from creating extra constraints.

We create each text field, give it some dummy text, and set up constraints for its horizontal position and size:

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

We'll use a loop to create the spacers too, but we create the top and bottom spacers differently from the middle spacers, because we need to pin the top and bottom spacers to the superview:

- (void)addSpacers {
    [self addTopSpacer];
    for (int i = 1, count = textFields.count; i < count; ++i) {
        [self addSpacerFromBottomOfView:textFields[i - 1]
            toTopOfView:textFields[i]];
    }
    [self addBottomSpacer];
}

Here's how we create the top spacer and set up its constraints. Its top edge is pinned to the superview and its bottom edge is pinned to the first (topmost) text field. We store the top spacer in the instance variable topSpacer so we can constrain the other spacers to have the same height as the top spacer.

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

Here's how we actually create a spacer view. It's just a hidden view. Since we don't care about its horizontal size or position, we just pin it to the left and right edges of the superview.

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

To create a “middle” spacer between two text views, we pin it to the bottom edge of the text field above and the top edge of the text field below. We also constrain its height to equal the height of the top 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)]];
}

To create the bottom spacer, we pin it to the last text field and to the superview. We also constrain its height to equal the height of the top spacer.

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

If you do it right, you will get a result like this:

example code screen shot

You can find a complete example project in this github repository.

rob mayoff
  • 342,380
  • 53
  • 730
  • 766
  • Thanks for this great answer. – CRDave Jun 14 '13 at 06:06
  • I am trying to shift on auto layout by thinking that it will be easy to implement. But it look very much to support only two view. And adding extra view also take memory and process power. But I think there is no other solution. So any way thanks for this. It help me to learn something. – CRDave Jun 14 '13 at 06:10
  • I'm confused: "you want auto layout to compute that c value for you. Auto layout simply can't do that". If `c` is the spacing then he's specifying it explicitly in his code... – Hari Honor Aug 01 '13 at 19:13
  • He doesn't *want* to specify it. He wants auto layout to compute it so he doesn't have to compute it himself. – rob mayoff Aug 01 '13 at 20:00
  • Great... So where did the objective of Apple to reduce the quantity of code for screen adaptation go ? – Plot Aug 02 '13 at 10:12
  • 1
    This answer (unfortunately) is wrong. You can achieve the desired effect without using spacer views. Check out the general case solution to this problem in the implementation of the method [`autoDistributeSubviews:alongAxis:withFixedSize:alignment:`](https://github.com/smileyborg/UIView-AutoLayout/blob/master/UIView%2BAutoLayout.m) on the [UIView+AutoLayout](https://github.com/smileyborg/UIView-AutoLayout) category I have created. – smileyborg Aug 19 '13 at 05:30
  • 4
    My answer doesn't require a fixed text field size. If the user can change the font, or if the app supports dynamic type under iOS 7, then my answer continues to lay out the views correctly with no additional code. My answer also works even if each text field has a different size. – rob mayoff Aug 19 '13 at 14:26
  • @rob mayoff Great answer. Pointed me in the right direction. I solved by using just IB / Storyboard, but relying on the conceptual you described. – marco alves Oct 21 '13 at 22:41
  • It's doable entirely in your xib or storyboard with Xcode 5. It wasn't doable with Xcode 4. – rob mayoff Oct 21 '13 at 22:58
  • I don't think so it's a good answer at least for me. The thing non digestible is that you adding and allocating UIView as a spacer!!!. Also it make me much more conflicting. Can you explain me what about performance and memory utilization footprint . – Tirth Apr 13 '15 at 15:04
  • The “performance and memory utilization footprint” are probably fine. If you think they are a problem, then **measure** the effect with Instruments. Profile, don't speculate. – rob mayoff Apr 13 '15 at 16:09
  • It is perhaps worth noting that `UIStackView` uses the technique I described here when you set its `distribution` to `.equalSpacing`, except that it uses `UILayoutGuide`s between the subviews instead of transparent `UIView`s. `UILayoutGuide` and `UIStackView` were added in iOS 9, two years after I wrote this answer. – rob mayoff Jun 09 '17 at 07:43
10

Check out PureLayout. It's designed to be the simplest and most programmer-friendly API possible for creating Auto Layout constraints in code.

In response to your specific question, PureLayout offers a two primary APIs for distributing views, one where the spacing between each view is fixed (view size varies as needed), and the other where the size of each view is fixed (spacing between views varies as needed). The latter will accomplish what you're looking for without the use of any "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;

// ...
smileyborg
  • 29,421
  • 10
  • 57
  • 72
  • I will try it. It seem like you have done a good job. Try to put it on www.cocoacontrols.com and cocoapods.org so that more people can see this. – CRDave Aug 19 '13 at 05:36
  • 2
    Thanks! I will definitely do that. Please feel free to let me know if you have any questions, feedback, suggestions, or find bugs! – smileyborg Aug 19 '13 at 05:38
2

see developer.apple' documentation, that is having nice description about the solution, https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/AutoLayoutbyExample/AutoLayoutbyExample.html see the spacing and warping in that page, i think that is nice description, so no need to explain same thing over here

EDIT

Above link is now get disabled by apple, As from iOS 9, they have introduced Stackview, which is solution for all this.

Previously in above link the answer was same as answer provided by @rob

Mehul Thakkar
  • 11,971
  • 8
  • 50
  • 74
  • yes i know... Apple have updated that document now.. Now for doing this equally spacing they introduced UIStackview..So, now it is not required to put spacerViews(which were described in that link).. you can now achieve same task easily using UIStackviews – Mehul Thakkar Jan 24 '16 at 15:25
  • The explanation at that link is almost same as the rob's answer.. so if you want to support ios version older than iOS 9.. then you need to go via that road only – Mehul Thakkar Jan 24 '16 at 15:28