How to use AutoLayout to position UIButtons in horizontal lines (wrapping, left aligned)? How to use AutoLayout to position UIButtons in horizontal lines (wrapping, left aligned)? xcode xcode

How to use AutoLayout to position UIButtons in horizontal lines (wrapping, left aligned)?


My current solution looks like this: No AutoLayout, but manually setting the correct constraints for each case (first button, leftmost button in a new line, any other button).

(My guess is that setting the frame for each button directly would result in more readable code than using NSLayoutConstraints, anyway)

NSArray *texts = @[ @"A", @"Short", @"Button", @"Longer Button", @"Very Long Button", @"Short", @"More Button", @"Any Key"];int indexOfLeftmostButtonOnCurrentLine = 0;NSMutableArray *buttons = [[NSMutableArray alloc] init];float runningWidth = 0.0f;float maxWidth = 300.0f;float horizontalSpaceBetweenButtons = 10.0f;float verticalSpaceBetweenButtons = 10.0f;for (int i=0; i<texts.count; i++) {    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];    [button setTitle:[texts objectAtIndex:i] forState:UIControlStateNormal];    [button sizeToFit];    button.translatesAutoresizingMaskIntoConstraints = NO;    [self.view addSubview:button];    // check if first button or button would exceed maxWidth    if ((i == 0) || (runningWidth + button.frame.size.width > maxWidth)) {        // wrap around into next line        runningWidth = button.frame.size.width;        if (i== 0) {            // first button (top left)            // horizontal position: same as previous leftmost button (on line above)            NSLayoutConstraint *horizontalConstraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0f constant:horizontalSpaceBetweenButtons];            [self.view addConstraint:horizontalConstraint];            // vertical position:            NSLayoutConstraint *verticalConstraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop              multiplier:1.0f constant:verticalSpaceBetweenButtons];            [self.view addConstraint:verticalConstraint];        } else {            // put it in new line            UIButton *previousLeftmostButton = [buttons objectAtIndex:indexOfLeftmostButtonOnCurrentLine];            // horizontal position: same as previous leftmost button (on line above)            NSLayoutConstraint *horizontalConstraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:previousLeftmostButton attribute:NSLayoutAttributeLeft multiplier:1.0f constant:0.0f];            [self.view addConstraint:horizontalConstraint];            // vertical position:            NSLayoutConstraint *verticalConstraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:previousLeftmostButton attribute:NSLayoutAttributeBottom multiplier:1.0f constant:verticalSpaceBetweenButtons];            [self.view addConstraint:verticalConstraint];            indexOfLeftmostButtonOnCurrentLine = i;        }    } else {        // put it right from previous buttom        runningWidth += button.frame.size.width + horizontalSpaceBetweenButtons;        UIButton *previousButton = [buttons objectAtIndex:(i-1)];        // horizontal position: right from previous button        NSLayoutConstraint *horizontalConstraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:previousButton attribute:NSLayoutAttributeRight multiplier:1.0f constant:horizontalSpaceBetweenButtons];        [self.view addConstraint:horizontalConstraint];        // vertical position same as previous button        NSLayoutConstraint *verticalConstraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:previousButton attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];        [self.view addConstraint:verticalConstraint];    }    [buttons addObject:button];}


Instead of using Autolayout, you could just use a collection view which better options for you to lay out elements such as buttons.

It is better able to handle layouts under rotation as well.


Here is the another example of how we can implement wrapping layout with auto layout:

    @interface SCHorizontalWrapView : UIView        @property(nonatomic)NSMutableArray *wrapConstrains;    @end@implementation SCHorizontalWrapView {    CGFloat intrinsicHeight;    BOOL updateConstraintsCalled;}-(id)init {    self = [super init];    if (self) {        [UIView autoSetPriority:UILayoutPriorityDefaultHigh forConstraints:^{            [self autoSetContentCompressionResistancePriorityForAxis:ALAxisVertical];            [self autoSetContentCompressionResistancePriorityForAxis:ALAxisHorizontal];            [self autoSetContentCompressionResistancePriorityForAxis:ALAxisHorizontal];            [self autoSetContentCompressionResistancePriorityForAxis:ALAxisVertical];        }];    }    return self;}-(void)updateConstraints {    if (self.needsUpdateConstraints) {        if (updateConstraintsCalled == NO) {            updateConstraintsCalled = YES;            [self updateWrappingConstrains];            updateConstraintsCalled = NO;        }        [super updateConstraints];    }}-(NSMutableArray *)wrapConstrains {    if (_wrapConstrains == nil) {        _wrapConstrains = [NSMutableArray new];    }    return _wrapConstrains;}-(CGSize)intrinsicContentSize {    return CGSizeMake(UIViewNoIntrinsicMetric, intrinsicHeight);}-(void)setViews:(NSArray*)views {    if (self.wrapConstrains.count > 0) {        [UIView autoRemoveConstraints:self.wrapConstrains];        [self.wrapConstrains removeAllObjects];    }    NSArray *subviews = self.subviews;    for (UIView *view in subviews) {        [view removeFromSuperview];    }    for (UIView *view in views) {        view.translatesAutoresizingMaskIntoConstraints = NO;        [self addSubview:view];        CGFloat leftPadding = 0;        [view autoSetDimension:ALDimensionWidth toSize:CGRectGetWidth(self.frame) - leftPadding relation:NSLayoutRelationLessThanOrEqual];    }}-(void)updateWrappingConstrains {    NSArray *subviews = self.subviews;    UIView *previewsView = nil;    CGFloat leftOffset = 0;    CGFloat itemMargin = 5;    CGFloat topPadding = 0;    CGFloat itemVerticalMargin = 5;    CGFloat currentX = leftOffset;    intrinsicHeight = topPadding;    int lineIndex = 0;    for (UIView *view in subviews) {        CGSize size = view.intrinsicContentSize;        if (previewsView) {            [self.wrapConstrains addObject:[view autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:topPadding relation:NSLayoutRelationGreaterThanOrEqual]];            [self.wrapConstrains addObject:[view autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:leftOffset relation:NSLayoutRelationGreaterThanOrEqual]];            CGFloat width = size.width;            currentX += itemMargin;            if (currentX + width <= CGRectGetWidth(self.frame)) {                [self.wrapConstrains addObject:[view autoConstrainAttribute:ALEdgeLeading toAttribute:ALEdgeTrailing ofView:previewsView withOffset:itemMargin relation:NSLayoutRelationEqual]];                [self.wrapConstrains addObject:[view autoAlignAxis:ALAxisBaseline toSameAxisOfView:previewsView]];                currentX += size.width;            }else {                [self.wrapConstrains addObject: [view autoConstrainAttribute:ALEdgeTop toAttribute:ALEdgeBottom ofView:previewsView withOffset:itemVerticalMargin relation:NSLayoutRelationGreaterThanOrEqual]];                currentX = leftOffset + size.width;                intrinsicHeight += size.height + itemVerticalMargin;                lineIndex++;            }        }else {            [self.wrapConstrains addObject:[view autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:topPadding relation:NSLayoutRelationEqual]];            [self.wrapConstrains addObject:[view autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:leftOffset relation:NSLayoutRelationEqual]];            intrinsicHeight += size.height;            currentX += size.width;        }        [view setNeedsUpdateConstraints];        [view updateConstraintsIfNeeded];        [view setNeedsLayout];        [view layoutIfNeeded];        previewsView = view;    }    [self invalidateIntrinsicContentSize];}@end

Here I'm using PureLayout for defining constrains.

You can use this class like this:

SCHorizontalWrapView *wrappingView = [[SCHorizontalWrapView alloc] initForAutoLayout];//parentView is some view[parentView addSubview:wrappingView];[tagsView autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:padding];[tagsView autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:padding];[tagsView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:locationView withOffset:padding relation:NSLayoutRelationGreaterThanOrEqual];[tagsView setNeedsLayout];[tagsView layoutIfNeeded];[tagsView setNeedsUpdateConstraints];[tagsView updateConstraintsIfNeeded];NSMutableArray *views = [NSMutableArray new];//texts is some array of nsstringsfor (NSString *text in texts) {    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];    button.translatesAutoresizingMaskIntoConstraints = NO;    [button setTitle:text forState:UIControlStateNormal];    button.backgroundColor = [UIColor lightGrayColor];    [views addObject:button];}[tagsView setViews:views];