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:
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];
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.