Evenly spacing several rows of an arbitrary number of elements using NSLayoutConstraints

autolayout, ios, nslayoutconstraint, objective-c

Solution

Something like this should work I think. I tried to make in general, so it should work for different number of rows and elements per row.

#define subviewHeight 44
#define spaceFromTop 10
#define spaceFromSide 10
#define subviewVerticalSpacing 10
#define subviewHorizontalSpacing 10

@implementation ViewController

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    UIButton *subview1 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [subview1 setTitle:@"View 1" forState:UIControlStateNormal];
    UIButton *subview2 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [subview2 setTitle:@"View 2" forState:UIControlStateNormal];
    UIButton *subview3 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [subview3 setTitle:@"View 3" forState:UIControlStateNormal];
    UIButton *subview4 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [subview4 setTitle:@"View 4" forState:UIControlStateNormal];
    UIButton *subview5 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [subview5 setTitle:@"View 5" forState:UIControlStateNormal];
    UIButton *subview6 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [subview6 setTitle:@"View 6" forState:UIControlStateNormal];

    NSArray *arrayOfSubviews = @[@[subview1, subview2, subview3], @[subview4], @[subview5, subview6]];

    [self addSubviewsWithConstraints:arrayOfSubviews];

}

-(void)addSubviewsWithConstraints:(NSArray *) arrayOfArrays {
    NSMutableArray *arrayOfViewsDicts = [NSMutableArray array]; // make the views dictionaries needed for the views parameter of constraintsWithVisualFormat:options:metrics:views:. One for each row.

    for (NSArray *array in arrayOfArrays) {
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        for (int i = 0; i < array.count; i++) {
            [dict setObject:array[i] forKey:[NSString stringWithFormat:@"view%d",i]];
        }
        [arrayOfViewsDicts addObject:dict];
    }

    for (int i=0; i<arrayOfArrays.count; i++) {
        NSString *formatString = [self makeConstraintsForRowOfSubviews:arrayOfArrays[i] withViewsDict:arrayOfViewsDicts[i]]; // Make the format string for a row of subviews

        for (id subview in arrayOfArrays[i]) {
            [subview setTranslatesAutoresizingMaskIntoConstraints:NO];
            [self.view addSubview:subview];
            NSLayoutConstraint *heightCon = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeHeight relatedBy:0 toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:subviewHeight];
            [subview addConstraint:heightCon];

            NSLayoutConstraint *verticalCon = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeTop relatedBy:0 toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:spaceFromTop + ((subviewHeight + subviewVerticalSpacing)*i)];
            [self.view addConstraint:verticalCon];
        }
        NSArray *cons = [NSLayoutConstraint constraintsWithVisualFormat:formatString options:NSLayoutFormatAlignAllBottom metrics:nil views:arrayOfViewsDicts[i]];

        [self.view addConstraints:cons];
    }
}


-(NSString *) makeConstraintsForRowOfSubviews:(NSArray *) arrayOfSubviews withViewsDict:(NSDictionary *) viewsDict {
    NSMutableString *formatString = [NSMutableString string];

    for (int i = 0; i<arrayOfSubviews.count; i++) {
        if (i == 0) {
            [formatString appendFormat:@"|-%d-[%@]",spaceFromSide,[viewsDict allKeysForObject:arrayOfSubviews[i]][0]];
        }else{
            [formatString appendFormat:@"-%d-[%@(==view0)]",subviewHorizontalSpacing,[viewsDict allKeysForObject:arrayOfSubviews[i]][0]];
        }
    }

    [formatString appendFormat:@"-%d-|",spaceFromSide];
    return formatString;
}

This code produced this result:

Problem

I have a question about using layout constraints for the following scenario. Everything is in code (no nibs or storyboards). I want to create a view that contains an arbitrary number of rows of items. Each row contains an arbitrary number of subviews. To create this view, I want to pass in an array that is two levels deep. The first level array contains each row. The second level array contains the elements in each row. So, for example, this might look like this: `NSArray *elements = @[@[subview1, subview2, subview3], @[subview4], @[subview5, subview6]]` In this array, there would be 3 rows: 1) Row 1: subview1, subview2, subview3 2) Row 2: subview4 3) Row 3: subview5, subview6 I want these elements to be formatted as: -The rows should all be the full width of their parent view (for this we can assume it's the size of the screen) -Each of the elements in a row should be the same width and have the same amount of space between them (say if there are 4 elements, the space between 1&2 and 2&3 could be 10pt) -Each of the rows should have the same amount of vertical space between them (e.g. 10pt vertical space between each row) In the scenario above, row 1 would have 3 subviews with equal width equally spaced apart, row 2 would have 1 subview which takes up the full width of the row, and row 3 would have 2 subviews that have equal width and would be equally spaced apart. So, the question is, how do I do this?! I've been working on this for awhile and my understanding doesn't seem to be getting any better. Help would be appreciated!

Original source