I need to create a couple of UIButtons with various widths programmatically in my app (iOS 6.0 and above).
I want to display the buttons in a "wrap around" style: Starting from the left edge, each button should be positioned next to each other horizontally (in a defined order), and if a button does not fit in the current "line", it should start a new line on the left edge below the previous line.
Note: I don't want a table/grid, since the buttons have different widths, and I want to have one right next to each other.

I could manually calculate the frame of each button in my code, but should I use AutoLayout (with programmatically created NSLayoutConstraints) instead? How exactly would I need to set it up?
EDIT: After reading through Chapter 4 "Intermediate Auto Layout" of "iOS 6 by Tutorials" I am not sure whether using pure AutoLayout could implement this "wrap around" functionality I require.
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 nsstrings
for (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];
来源:https://stackoverflow.com/questions/19808739/how-to-use-autolayout-to-position-uibuttons-in-horizontal-lines-wrapping-left