NSLayoutConstraint for unknown number of subviews in code - ios

I'm getting into iOS programming and mastered more or less autolayout with fixed number of items. Say, we have a UILabel for title, UILabel for subtitle, then the visual format for the constraint is 'V:|-[title]-10-[subtitle]-|'
But what if I create subviews dynamically based on some API response. There are for example 40 subviews I need to add. It's not realistic anymore that I specify each subview with the visual format and keep track of them. What is the the proper way?
I imagine that after each subview I add, I then set the constraint based on the previous view with constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:. Is that the way to go, or there is a better way?

It's not realistic anymore that I specify each subview with the visual format and keep track of them
Why on earth not? You seem to think this is some sort of "either/or" situation. Nothing prevents you from using visual formatting to build up a collection of constraints. No law says that you have to put all your constraints into one visual format.
And the layout engine doesn't know or care what notation you used to form your constraints. A constraint is a constraint!
Consider this code where I build up the constraints for a scroll view and thirty labels inside it:
var con = [NSLayoutConstraint]()
con.appendContentsOf(
NSLayoutConstraint.constraintsWithVisualFormat(
"H:|[sv]|",
options:[], metrics:nil,
views:["sv":sv]))
con.appendContentsOf(
NSLayoutConstraint.constraintsWithVisualFormat(
"V:|[sv]|",
options:[], metrics:nil,
views:["sv":sv]))
var previousLab : UILabel? = nil
for i in 0 ..< 30 {
let lab = UILabel()
// lab.backgroundColor = UIColor.redColor()
lab.translatesAutoresizingMaskIntoConstraints = false
lab.text = "This is label \(i+1)"
sv.addSubview(lab)
con.appendContentsOf(
NSLayoutConstraint.constraintsWithVisualFormat(
"H:|-(10)-[lab]",
options:[], metrics:nil,
views:["lab":lab]))
if previousLab == nil { // first one, pin to top
con.appendContentsOf(
NSLayoutConstraint.constraintsWithVisualFormat(
"V:|-(10)-[lab]",
options:[], metrics:nil,
views:["lab":lab]))
} else { // all others, pin to previous
con.appendContentsOf(
NSLayoutConstraint.constraintsWithVisualFormat(
"V:[prev]-(10)-[lab]",
options:[], metrics:nil,
views:["lab":lab, "prev":previousLab!]))
}
previousLab = lab
}
con.appendContentsOf(
NSLayoutConstraint.constraintsWithVisualFormat(
"V:[lab]-(10)-|",
options:[], metrics:nil,
views:["lab":previousLab!]))
NSLayoutConstraint.activateConstraints(con)
I'm using visual formatting, but I'm doing it one constraint as a time as I add views (which is exactly what you're asking about).

The answer really depends on what layout you want, because there are higher-level tools for making some layouts.
Starting with iOS 9, you can use UIStackView to manage a row or column of views. By nesting stack views, you can easily make a grid of views. For an introduction to UIStackView, start by watching “Mysteries of Auto Layout, Part 1” from WWDC 2015. If you're doing all your layout in code, you can use TZStackView, a reimplementation of UIStackView that works on iOS 7 and later.
UICollectionView is another tool for laying out views in a grid, or in many other arrangements. It's more complex than UIStackView, but there's a lot of introductory material available to teach you how to use it, such as “Introducing Collection Views” from WWDC 2012.

I built a dynamic layout engine (Horizontal layout and Vertical layout).
It accepts an array of views.
It adds the views as subviews to a container.
Then composes a vertical and horizontal Visual layout string dynamically. (It generates a unique "Object Address" string for each view being laid out using v%p.
Simply call: [myView addControls:#[view1, view2, view3]] in viewDidLoad, and it will lay them out vertically for you.
Here's a generic layout engine for a vertical layout. Enjoy.
#implementation UIView (VerticalLayout)
- (void)addControls:(NSArray*)controls
align:(VerticalAlignment)alignment
withHeight:(CGFloat)height
verticalPadding:(CGFloat)verticalPadding
horizontalPadding:(CGFloat)horizontalPadding
topPadding:(CGFloat)topPadding
bottomPadding:(CGFloat)bottomPadding
withWidth:(CGFloat)width
{
NSMutableDictionary* bindings = [[NSMutableDictionary alloc] init];
NSDictionary *metrics = #{
#"topPadding": #(topPadding),
#"bottomPadding": #(bottomPadding),
#"verticalPadding": #(verticalPadding),
#"horizontalPadding":#(horizontalPadding) };
NSMutableString* verticalConstraint = [[NSMutableString alloc] initWithString:#"V:"];
if (alignment == VerticalAlignTop || alignment == VerticalAlignStretchToFullHeight) {
[verticalConstraint appendString:#"|"];
}
for (UIView* view in controls) {
BOOL isFirstView = [controls firstObject] == view;
BOOL isLastView = [controls lastObject] == view;
[self addSubview: view];
// Add to vertical constraint string
NSString* viewName = [NSString stringWithFormat:#"v%p",view];
[bindings setObject:view forKey:viewName];
NSString* yLeadingPaddingString = #"";
NSString* yTrailingPadding = #"";
if (isFirstView && topPadding > 0) {
yLeadingPaddingString = #"-topPadding-";
}
else if (!isFirstView && verticalPadding > 0) {
yLeadingPaddingString = #"-verticalPadding-";
}
if (isLastView && bottomPadding > 0) {
yTrailingPadding = #"-bottomPadding-";
}
[verticalConstraint appendFormat:#"%#[%#%#]%#",
yLeadingPaddingString,
viewName,
height > 0 ? [NSString stringWithFormat:#"(%f)", height] : #"",
yTrailingPadding];
NSArray* constraints = [NSLayoutConstraint constraintsWithVisualFormat:
[NSString stringWithFormat: #"H:|-horizontalPadding-[%#%#]%#|",
viewName,
(width > 0 ? [NSString stringWithFormat:#"(%lf)", width] : #""),
width > 0 ? #"-" : #"-horizontalPadding-"]
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:metrics
views:bindings];
// high priority, but not required.
for (NSLayoutConstraint* constraint in constraints) {
constraint.priority = 900;
}
// Add the horizontal constraint
[self addConstraints: constraints];
}
if (alignment == VerticalAlignBottom || alignment == VerticalAlignStretchToFullHeight) {
[verticalConstraint appendString:#"|"];
}
NSArray* constraints = [NSLayoutConstraint
constraintsWithVisualFormat:verticalConstraint
options:NSLayoutFormatAlignAllLeading
metrics:metrics
views:bindings];
for (NSLayoutConstraint* constraint in constraints) {
constraint.priority = 900;
}
// Add the vertical constraints for all these views.
[self addConstraints:constraints];
}

Related

How to set constraints programmatically for a varying number of UIView

I'm new to auto layout constraints. In my application there are 3 buttons in one line. Now I want to set the constraints programatically as shown in the image below:
If there is one, then the button show in center horizontally.
If two buttons are enable, then second line shown in same image as three image.
My approach is to setting all buttons centerX to superview's centerX at the beginning.
IBOutletCollection(UIButton) NSArray *allButtons;
//3 buttons' references
IBOutletCollection(NSLayoutConstraint) NSArray *allButtonCenterConstraints;
//buttons' centerX constraints' references
and then if in viewDidAppear:
int t = arc4random() %3;// one of the 3 cases you will need
if (t == 0) {//if you want all 3 buttons appears
//all buttons shown
NSLayoutConstraint *c1 = allButtonCenterConstraints[0];//first button's centerX reference.
NSLayoutConstraint *c2 = allButtonCenterConstraints[2];//third button's centerX reference.
c1.constant = -self.view.frame.size.width*0.25;//push left
c2.constant = +self.view.frame.size.width*0.25;//push right
}
else if (t == 1)
{
//two buttons shown;
[allButtons[0] setHidden:YES];// close the one you dont need
NSLayoutConstraint *c1 = allButtonCenterConstraints[1];
NSLayoutConstraint *c2 = allButtonCenterConstraints[2];
c1.constant = -self.view.frame.size.width*0.125;
c2.constant = +self.view.frame.size.width*0.125;
}
else
{
//close 2 buttons
[allButtons[0] setHidden:YES];
[allButtons[1] setHidden:YES];
}
[self.view layoutIfNeeded];
if you need something depends on the width of buttons, you can set the constants according to the width of buttons.
Make constraint outlet after that you set constraint pragmatically
like as :
#IBOutlet var mainViewWidthConstant: NSLayoutConstraint!
if (self.view.frame.size.width == 320){
mainViewWidthConstant.constant = 320
}
else if (self.view.frame.size.width == 375)
{
mainViewWidthConstant.constant = 375
}
else{
mainViewWidthConstant.constant = 414
}

AutoLayout Visual language for mixed V+H

How can view layouts such as this, where the subviews are in vertical and horizontal relationships, be described using the Visual language?
Here is one way to do it. Assumptions: views 1 and 2 have a fixed pixel width, view 3 fills remaining width, fixed margin all round and between views. Views 1 and 2 equal height.
If those are wrong assumptions its pretty straightforward to extend this example.
Swift:
let views = ["view1": view1, "view2": view2, "view3": view3]
let metrics = ["m":12,"w":100]
let format0 = "H:|-(m)-[view1(w)]-(m)-[view3]-(m)-|"
let format1 = "H:|-(m)-[view2(w)]-(m)-[view3]-(m)-|"
let format2 = "V:|-(m)-[view1]-(m)-[view2(==view1)]-(m)-|"
let format3 = "V:|-(m)-[view3]-(m)-|"
for string in [format0,format1,format2,format3] as [String] {
self.view.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat(
string,
options:nil,
metrics:metrics,
views:views))
}
Objective-C:
NSDictionary* views = NSDictionaryOfVariableBindings(view1,view2,view3);
NSDictionary* metrics = #{#"m":#(12),#"w":#(100)};
NSString* format0 = #"H:|-m-[view1(w)]-m-[view3]-m-|";
NSString* format1 = #"H:|-m-[view2(w)]-m-[view3]-m-|";
NSString* format2 = #"V:|-m-[view1]-m-[view2(==view1)]-m-|";
NSString* format3 = #"V:|-m-[view3]-m-|";
for (NSString* string in #[format0,format1,format2,format3]) {
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:string
options:0
metrics:metrics
views:views]];
}
The views under autolayout control need to have their translatesAutoresizingMaskIntoConstraints property set to NO (it defaults to YES).
In one of your comments you say that the views 'already know their frames'. This sounds a little confused: when using autolayout, views don't set frames, frames are the result of the autolayout equations (the autolayout mechanism sets them).
In any case whether or not you use autolayout, views shouldn't set their own frames, that should be the job of the view's superview context. The superview, or its viewController would make frame decisions, as a frame positions a view with respect to the superview.
It sounds like you may mean that the views already know their sizes, based on their content (in the same way that buttons and labels know their sizes). In this case they can return a size value by overriding -(CGSize) intrinsicContentSize in a UIView subclass. Then you can then omit size metrics from the format strings, simplifying them to:
Swift:
let format0 = "H:|-m-[view1]-m-[view3]-m-|"
let format1 = "H:|-m-[view2]-m-[view3]-m-|"
let format2 = "V:|-m-[view1]-m-[view2]-m-|"
let format3 = "V:|-m-[view3]-m-|"
Objective-C:
NSString* format0 = #"H:|-m-[view1]-m-[view3]-m-|";
NSString* format1 = #"H:|-m-[view2]-m-[view3]-m-|";
NSString* format2 = #"V:|-m-[view1]-m-[view2]-m-|";
NSString* format3 = #"V:|-m-[view3]-m-|";
However if the sizes don't all add up (eg 3*m + view1.height + view2.height != superview.height) something's going to break, and you are losing the advantage of using autolayout to flexibly arrange your views to fill the available space.

Disable Autolayout Localization Behavior (RTL - Right To Left Behavior )

My application is localized in both English and Arabic.
Unfortunately, sometimes the autolayout behavior for localization is not required. By that, I mean reversing the leading and trailing spaces. I want to override this behavior. Is there any way to do that?
To make leading act always as left (and trailing always like right), i.e. make it language independent, you can remove checkmark on "Respect language direction" on all constrains.
You can find this checkmark in constrain settings in the Attributes inspector under "First Item" button.
The attributes leading and trailing are the same as left and right for left-to-right languages such as English, but in a right-to-left environment such as Hebrew or Arabic, leading and trailing are the same as right and left. When you create constraints, leading and trailing are the default values. You should usually use leading and trailing to make sure your interface is laid out appropriately in all languages, unless you’re making constraints that should remain the same regardless of language.
So, for your special cases, don't use leading and trailing, instead, explicitly use left and right when you create your constraints.
as in #Pavel answer, you should turn off 'Respect language direction' property. if you have lots of constraints, you can open xib or storyboard file in XML view and replace all 'leading' values with 'left' and all 'trailing' values with 'right' and you're done.
Try this
Create class for managing constraint in all views
#implementation RTLController
#pragma mark - Public
- (void)disableRTLForView:(UIView *)view
{
[self updateSubviewForParentViewIfPossible:view];
}
#pragma mark - Private
- (void)updateConstraintForView:(UIView *)view
{
NSMutableArray *constraintsToRemove = [[NSMutableArray alloc] init];
NSMutableArray *constraintsToAdd = [[NSMutableArray alloc] init];
for (NSLayoutConstraint *constraint in view.constraints) {
NSLayoutAttribute firstAttribute = constraint.firstAttribute;
NSLayoutAttribute secondAttribute = constraint.secondAttribute;
if (constraint.firstAttribute == NSLayoutAttributeLeading) {
firstAttribute = NSLayoutAttributeLeft;
} else if (constraint.firstAttribute == NSLayoutAttributeTrailing) {
firstAttribute = NSLayoutAttributeRight;
}
if (constraint.secondAttribute == NSLayoutAttributeLeading) {
secondAttribute = NSLayoutAttributeLeft;
} else if (constraint.secondAttribute == NSLayoutAttributeTrailing) {
secondAttribute = NSLayoutAttributeRight;
}
NSLayoutConstraint *updatedConstraint = [self constraintWithFirstAttribute:firstAttribute secondAtribute:secondAttribute fromConstraint:constraint];
[constraintsToRemove addObject:constraint];
[constraintsToAdd addObject:updatedConstraint];
}
for (NSLayoutConstraint *constraint in constraintsToRemove) {
[view removeConstraint:constraint];
}
for (NSLayoutConstraint *constraint in constraintsToAdd) {
[view addConstraint:constraint];
}
}
- (void)updateSubviewForParentViewIfPossible:(UIView *)mainView
{
NSArray *subViews = mainView.subviews;
[self updateConstraintForView:mainView];
if (subViews.count) {
for (UIView * subView in subViews) {
[self updateConstraintForView:subView];
[self updateSubviewForParentViewIfPossible:subView];
}
}
}
- (NSLayoutConstraint *)constraintWithFirstAttribute:(NSLayoutAttribute)firstAttribute secondAtribute:(NSLayoutAttribute)secondAttribute fromConstraint:(NSLayoutConstraint *)originalConstraint
{
NSLayoutConstraint *updatedConstraint =
[NSLayoutConstraint constraintWithItem:originalConstraint.firstItem
attribute:firstAttribute
relatedBy:originalConstraint.relation
toItem:originalConstraint.secondItem
attribute:secondAttribute
multiplier:originalConstraint.multiplier
constant:originalConstraint.constant];
return updatedConstraint;
}
#end
Add this code in to controller that should disable RTL behavior
RTLController *rtl = [[RTLController alloc] init];
[rtl disableRTLForView:self.view];
another easy way
Select any view from the storyboard
from the attributes inspector on the right select set "Semantic" to "Force Left-to-Right"

Auto layout visual programming language for an array of views

Let's say I have an array of views, and I want to stack these views in a list. Now, if I know ahead of time how many views there are, I could write a constraint like this:
"V:|-[view0]-[view1]-[view2]-[view_n]"
However, how can I accomplish something like this with a variable number of views in my array?
You can iterate over the array and build the string (use an NSMutableString). You need to add the views to a dictionary with keys that match the names you use in the format string.
Check out this awesome category:
https://github.com/jrturton/UIView-Autolayout
It has a spaceViews method that you can call on the container view and will space an array of views evenly along the specified axis.
There's some sample code in the demo project that should cover everything.
Here is how you would space some views evenly over the vertical axis:
This centre 4 views on the x axis and constrain the width to 150 points. The height would then be calculated depending on the height of self.view
#import "UIView+AutoLayout.h"
...
- (void)spaceViews
{
NSArray *views = #[ [self spacedView], [self spacedView], [self spacedView], [self spacedView] ];
[self.view spaceViews:views onAxis:UILayoutConstraintAxisVertical withSpacing:10 alignmentOptions:0];
}
- (UIView *)spacedView
{
//Create an autolayout view
UIView *view = [UIView autoLayoutView];
//Set the backgroundColor
[view setBackgroundColor:[UIColor redColor]];
//Add the view to the superview
[self.view addSubview:view];
//Constrain the width and center on the x axis
[view constrainToSize:CGSizeMake(150, 0)];
[view centerInContainerOnAxis:NSLayoutAttributeCenterX];
//Return the view
return view;
}
I had a requirement to add views from an array to a scrollview for my tutorial pages, in this case I built the VFL string by looping through the views, below is a snapshot, this code is to fit subview fully into scrollview's page. With some tweaks, padding,etc can be added. Anyways posting it here so that it helps someone.
Full code arrayAutolayout
/*!
Create an array of views that we need to load
#param nil
#result creates array of views and adds it to scrollview
*/
-(void)setUpViews
{
[viewsDict setObject:contentScrollView forKey:#"parent"];
int count = 20;//Lets layout 20 views
for (int i=0; i<=count; i++) {
// I am loading the view from xib.
ContentView *contenView = [[NSBundle mainBundle] loadNibNamed:#"ContentView" owner:self options:nil][0];
contenView.translatesAutoresizingMaskIntoConstraints = NO;
// Layout the text and color
[contenView layoutTheLabel];
[contentScrollView addSubview:contenView];
[viewsArray addObject:contenView];
}
}
/*!
Method to layout the childviews in the scrollview.
#param nil
#result layout the child views
*/
-(void)layoutViews
{
NSMutableString *horizontalString = [NSMutableString string];
// Keep the start of the horizontal constraint
[horizontalString appendString:#"H:|"];
for (int i=0; i<viewsArray.count; i++) {
// Here I am providing the index of the array as the view name key in the dictionary
[viewsDict setObject:viewsArray[i] forKey:[NSString stringWithFormat:#"v%d",i]];
// Since we are having only one view vertically, then we need to add the constraint now itself. Since we need to have fullscreen, we are giving height equal to the superview.
NSString *verticalString = [NSString stringWithFormat:#"V:|[%#(==parent)]|", [NSString stringWithFormat:#"v%d",i]];
// add the constraint
[contentScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:verticalString options:0 metrics:nil views:viewsDict]];
// Since we need to horizontally arrange, we construct a string, with all the views in array looped and here also we have fullwidth of superview.
[horizontalString appendString:[NSString stringWithFormat:#"[%#(==parent)]", [NSString stringWithFormat:#"v%d",i]]];
}
// Close the string with the parent
[horizontalString appendString:#"|"];
// apply the constraint
[contentScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:horizontalString options:0 metrics:nil views:viewsDict]];
}
Below is the string created
H:|[v0(==parent)][v1(==parent)][v2(==parent)][v3(==parent)][v4(==parent)][v5(==parent)][v6(==parent)][v7(==parent)][v8(==parent)][v9(==parent)][v10(==parent)][v11(==parent)][v12(==parent)][v13(==parent)][v14(==parent)][v15(==parent)][v16(==parent)][v17(==parent)][v18(==parent)][v19(==parent)][v20(==parent)]|

iOS Auto Layout: Equal Spaces to Fit Superviews Width [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Autolayout Even Spacing
I'm trying to create a scrollable bar with buttons (similar to a UISegmentedControl). The superview is an UIScrollView. As soon as the buttons don't fit into the scrollview, the scrollview should be scrollable. So far, almost everything works fine:
With a lot of buttons (scrolled to the right):
Not so many buttons:
Now, my goal is that if there is room for all buttons, they should be equally spread across the whole 320px view. How can I define constrains for the spaces in between the buttons?
Right now, I'm using the following constraints (self is a UIScrollView):
UIView *firstButton = self.buttons[0];
[self.buttonConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"|-(5)-[firstButton]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(firstButton)]];
UIView *lastButton = [self.buttons lastObject];
[self.buttonConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"[lastButton]-(5)-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(lastButton)]];
UIView *previousView = nil;
for (UIView *view in self.buttons) {
if (previousView) {
[self.buttonConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"[previousView]-(5)-[view]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(previousView, view)]];
}
previousView = view;
}
If I change the type of the superview from UIScrollView to an UIView, I get the following result, still not what I want, but at least it looks for the constraint of the last button that ties it to the right edge (makes sense, that this doesn't happen for the scrollview, as the content size is automatically set):
Any ideas?
- (void) viewWillLayoutSubviews {
// UIButton *button1, *button2, *button3, *button 4 ;
// NSMutableArray *constraintsForButtons ;
float unusedHorizontalSpace = self.view.bounds.size.width - button1.intrinsicContentSize.width - button2.intrinsicContentSize.width - button3.intrinsicContentSize.width - button4.intrinsicContentSize.width ;
NSNumber* spaceBetweenEachButton= [NSNumber numberWithFloat: unusedHorizontalSpace / 5 ] ;
[self.view removeConstraints:constraintsForButtons] ;
[constraintsForButtons removeAllObjects] ;
[constraintsForButtons addObjectsFromArray: [NSLayoutConstraint constraintsWithVisualFormat: #"H:|-(space)-[button1]-(space)-[button2]-(space)-[button3]-(space)-[button4]-(space)-|"
options: NSLayoutFormatAlignAllCenterY
metrics: #{#"space":spaceBetweenEachButton}
views: NSDictionaryOfVariableBindings(button1,button2,button3,button4) ] ] ;
[constraintsForButtons addObjectsFromArray: [NSLayoutConstraint constraintsWithVisualFormat: #"V:|[button1]"
options: 0
metrics: nil
views: NSDictionaryOfVariableBindings(button1) ] ] ;
[self.view addConstraints:constraintsForButtons] ;
}
This isn't as pretty as yours, and it assumes there are 4 buttons, but it equally spaces them. That is, the empty spaces between the buttons all have the same width. This does not mean that the distance between the NSLayoutAttributeLeading of button1 and button2 is the same as the distance between button2 & button3's.

Resources