Does it matter which view constraints are added to? - ios

I find it convenient to collect all constraints for a whole view hierarchy and adding them all to the local root view. Is this discouraged and/or a bad idea versus adding constraints to the appropriate views?
Below is a simplification of a common pattern I use. The views are often more complicated with multiple stored constraints that needs updating as views are added or removed.
- (void)updateViewConstraints
{
UIView *content = ...;
UIView *panel = ...;
UILabel *label = ...;
[self.view addSubview:content];
[content addSubview:panel];
[panel addSubview:label];
NSMutableArray *constraints = [NSMutableArray new];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"|[content]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(content)]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"|[panel]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(panel)]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"|[label]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(label)]];
[self.view addConstraints:constraints]
self.storedConstraints = constraints;
[super updateViewConstraints];
}
- (void)invalidateConstraints
{
[self.view removeConstraints:self.storedConstraints];
[self.view needsUpdateConstraints];
}

Related

How to add two views on each other with constraints

I am trying to add 2 views with same X, Y. They have space from the edge using addConstraints:[NSLayoutConstraint constraintsWithVisualFormat...]
I tried the strings "H:|20-[A][B]-20-[C]-20-[D]-20|" and V:|20-[A][B]-20-[C]-20-[D]-20|, and centreX == CenterX and centerY==centerY, but they kept conflicting, thinking that A and B should be next to each other.
A is hidden, and on some button clicked, B is hidden and A is shown.
To add views with same centre X,Y You need to first place one of the view say bView (Bottom view), using constatins
NSArray * bVerticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|-20-[bView]-20-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(bView)];
NSArray * bHorizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-20-[bView]-20-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(bView)];
This places the bView in the superview with 20 as edge offset.
Now place the tView (top View) with same centre as of bView. Here self is the superview of bView and tView
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[bView]-(<=1)-[tView]" options:NSLayoutFormatAlignAllCenterX metrics:nil views:NSDictionaryOfVariableBindings(bView,tView)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:[bView]-(<=1)-[tView]" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(bView,tView)]];
Then pin the edges of tView with desired offset say 40
NSArray * tVerticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|-40-[tView]-40-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(tView)];
NSArray * tHorizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-40-[tView]-40-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(tView)];
EDIT :
Here is how do do it.
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
bView = [UIView new];
bView.backgroundColor = [UIColor redColor];
bView.translatesAutoresizingMaskIntoConstraints = NO;
tView = [UIView new];
tView.backgroundColor = [UIColor blackColor];
tView.translatesAutoresizingMaskIntoConstraints = NO;
// Firdt bView is added then tView hence tView is exaclty above the bView.
[self.view addSubview:bView];
[self.view addSubview:tView];
// Edges Constrints for bView
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-20-[bView]-20-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(bView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-20-[bView]-20-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(bView)]];
// Edges Constints for tView
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-20-[tView]-20-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(tView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-20-[tView]-20-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(tView)]];
// Centring for bView and tView
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[bView]-(<=1)-[tView]" options:NSLayoutFormatAlignAllCenterX metrics:nil views:NSDictionaryOfVariableBindings(bView,tView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:[bView]-(<=1)-[tView]" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(bView,tView)]];
}
-(IBAction)toggleViews:(id)sender {
NSArray * subViews = self.view.subviews;
[self.view exchangeSubviewAtIndex:[subViews indexOfObject:tView] withSubviewAtIndex:[subViews indexOfObject:bView]];
}

Adding programatically created views into scrollview vertically (Linear layout in iOS)

I want to add programatically created UIViews into scrollView with auto layout constraints. Like vertical linear layout in Android.
(In objective c not swift)
I have scrollview inside view controller in storyboard. So basically i want to create and add several views in vertical layout with no spaces into that scrollview. And i want to set container size of the scroll view dynamically according to the view heights.
Each view has label inside and each view needs to set its height dynamically according to text size. But probably i need to come to that later.
for (int i=0; i<10; i++)
{
UIView *viewOne = UIView.new;
[viewOne setTranslatesAutoresizingMaskIntoConstraints:NO];
viewOne.backgroundColor = [UIColor redColor];
NSDictionary *viewsDictionary = #{#"viewOne" : viewOne};
NSDictionary *metricsDictionary = #{#"horizontalSpacing" : #10};
[self.scrollview addSubview:viewOne];
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-horizontalSpacing-[viewOne]-horizontalSpacing-|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:metricsDictionary
views:viewsDictionary];
NSArray *const_Height = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[viewOne(50)]"
options:0
metrics:nil
views:viewsDictionary];
[viewOne addConstraints:const_Height];
[self.scrollview addConstraints:horizontalConstraints];
}
With that code i can add views but i need to add one under the other.
In case of using AutoLayout in the context of a UIScrollView I would recommend using a ContentView insider your UIScrollView. Just add them to the ViewControllers View inside the viewDidLoad function.
#interface YourViewController ()
#property (nonatomic, strong) UIScrollView *dataScrollView;
#property (nonatomic, strong) UIView* contentView;
#end
#implementation YourViewController
#synthesize dataScrollView, contentView;
- (void) viewDidLoad {
[super viewDidLoad];
dataScrollView = [[UIScrollView alloc] init];
contentView = [[UIView alloc] init];
// adding the Views programmatically to the hierarchy
[self.view addSubview:dataScrollView];
[dataScrollView addSubview:contentView];
// don't translate the AutoresizingMask into constraints
dataScrollView.translatesAutoresizingMaskIntoConstraints = NO;
contentView.translatesAutoresizingMaskIntoConstraints = NO;
// backgroundColor as you wish?
dataScrollView.backgroundColor = [UIColor clearColor];
contentView.backgroundColor = [UIColor clearColor];
[dataScrollView setScrollEnabled:YES];
[dataScrollView setAlwaysBounceVertical:YES];
NSDictionary* viewsDictionary = NSDictionaryOfVariableBindings(dataScrollView, contentView);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[dataScrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[dataScrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[dataScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[contentView(==dataScrollView)]|" options:0 metrics: 0 views:viewsDictionary]];
[dataScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[contentView]|" options:0 metrics: 0 views:viewsDictionary]];
// see below
// [self setUpViews];
}
This Code will do the Trick for one single view. Add your required Views as Subview to the contentView and set the Constraints.
- (void) setUpViews {
UILabel* testLabel = [[UILabel alloc] init];
[testLabel setText:#"Lorem Ipsum"];
testLabel.translatesAutoresizingMaskIntoConstraints = NO;
[contentView addSubview: testLabel];
// clean up your code with this metrics Dictionary
NSDictionary *metrics = #{#"margintop": #40,
#"marginleft": #10,
#"marginright": #10,
#"marginbottom": #20}
// the Views we want to layout with Constraints
NSDictionary *viewsDictionary = #{
#"contentView":contentView,
#"dataScrollView":dataScrollView,
#"testLabel": testLabel}
// Horizontal (testlabel)
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-marginleft-[testLabel]-marginright-|" options:0 metrics: metrics views:viewsDictionary]];
// Vertical
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-margintop-[testLabel]-marginbottom-|" options:0 metrics: metrics views:viewsDictionary]];
}
Referring to your question of adding multiple Views in a for-loop, there are a lot of possible ways. This could be the easiest solution with constraintsWithVisualFormat.
- (void) setUpViews {
NSDictionary *metrics = #{#"margintop": #40,
#"marginleft": #10,
#"marginright": #10,
#"marginbottom": #20,
};
// Alsways like to have contentView and DataScrollView here
NSMutableDictionary* dictViews = [[NSMutableDictionary alloc] initWithDictionary:#{#"contentView":contentView,
#"dataScrollView":dataScrollView}];
// Basic Leading-String for Vertical Constraints
NSString* verticalConstraintsString = #"V:|-margintop-";
for (NSUInteger index = 0; index < 10; index++) {
// Do your Magic here & add your View
UILabel* testLabel = [[UILabel alloc] init];
[testLabel setText:#"Lorem Ipsum"];
testLabel.translatesAutoresizingMaskIntoConstraints = NO;
[contentView addSubview: testLabel];
// Add to global Mutable Views-Dictionary dictViews
[dictViews setObject:testLabel forKey:[NSString stringWithFormat:#"testLabel%lu", (unsigned long)index]];
// add "[testlabel1]" to the vertical Constraints
verticalConstraintsString = [NSString stringWithFormat:#"%#[testLabel%lu]-", verticalConstraintsString, (unsigned long)index];
// Add Horizontal Constraints
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:#"H:|-marginleft-[testLabel%lu]-marginright-|", (unsigned long)index] options:0 metrics: metrics views:#{#"testLabel-%lu":testLabel}]];
}
// Trailing-String
verticalConstraintsString = [NSString stringWithFormat:#"%#marginbottom-|", verticalConstraintsString];
NSDictionary *viewsDictionary = [[NSDictionary alloc] initWithDictionary:dictViews];
// finally adding the vertical Constraints
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:verticalConstraintsString options:0 metrics: metrics views:viewsDictionary]];
}
I hope this will help you to get your Views right.

How to draw a horizontal line using UIView and autolayout?

Here is the code I have...this is kind of a simple question...perhaps I shouldn't do it with UIView...but I just wanna draw a decorative horizontal line to separate content. What I am getting is no error the line just does not draw. If I init it with a frame and turn off autolayout, then I do see the line but that's not how I want to implement it since everything is autolayout.
Below the viewDidLoad calls the other methods to create the UI elements and add the constraints.
- (void)viewDidLoad {
[super viewDidLoad];
// Add main layout
[self.scrollView addSubview:self.profileInfoContainer];
// Add UI elements
[self.profileInfoContainer addSubview:self.email];
[self.profileInfoContainer addSubview:self.userName];
[self.profileInfoContainer addSubview:self.hLineSeperator];
[self.profileInfoContainer addSubview:self.navigationContainer];
// Add Constraints
[self addInfoContainerConst];
}
Below i add the constraints for all ui elements in the profileInfoContainer, including the hLineSeperator
- (void)addInfoContainerConst {
// Add constrains
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_userName, _email, _hLineSeperator, _navigationContainer);
NSDictionary *metrics = #{
#"vTop":#60,
#"vBttm":#5,
#"hLeft":#25,
#"hRight":#25,
#"vMiddle":#5
};
NSArray *constraint_POS_V = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-vTop-[_userName]-vMiddle-[_email]-vMiddle-[_hLineSeperator]-vMiddle-[_navigationContainer]|"
options:0
metrics:metrics
views:viewsDictionary];
NSArray *constraint_POS_H = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-hLeft-[_userName]-hRight-|"
options:0
metrics:metrics
views:viewsDictionary];
NSArray *constraint_POS_H_1 = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-hLeft-[_email]-hRight-|"
options:0
metrics:metrics
views:viewsDictionary];
NSArray *constraint_POS_H_2 = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_hLineSeperator(1)]|"
options:0
metrics:metrics
views:viewsDictionary];
NSArray *constraint_POS_H_3 = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_navigationContainer]|"
options:0
metrics:metrics
views:viewsDictionary];
[self.profileInfoContainer addConstraints:constraint_POS_V];
[self.profileInfoContainer addConstraints:constraint_POS_H];
[self.profileInfoContainer addConstraints:constraint_POS_H_1];
[self.profileInfoContainer addConstraints:constraint_POS_H_2];
[self.profileInfoContainer addConstraints:constraint_POS_H_3];
}
Below is the method to create the UIView that is used to draw the line
- (UIView*)hLineSeperator {
if (!_hLineSeperator) {
_hLineSeperator = [UIView new];
_hLineSeperator.translatesAutoresizingMaskIntoConstraints = NO;
_hLineSeperator.backgroundColor = [UIColor grayColor];
}
return _hLineSeperator;
}
It is not required to add constraints to subviews (specifically like the ones you want to create) created in code. You can do it something like,
UIView *horizontalSeperatorViewOnTop = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.(yourView).frame.size.width, 0.5f)];
horizontalSeperatorViewTop.backgroundColor = [UIColor lightGrayColor];
[self.(yourView) addSubview: horizontalSeperatorViewOnTop];
I do this in my code all the time for horizontal or vertical separators and it works great!

Vertically stack an array of buttons using auto layout VFL

I'm learning auto layout and I'd like to setup a set of buttons to be vertically stacked and evenly spaced. I'd also like the buttons pinned to the bottom of the view. What's a good way to setup these constraints with VFL? The button list will be passed in as an array of UIButtons.
NSArray *buttons = [button1, button2, button3, button4, ...]
NSMutableArray *allConstraints = [NSMutableArray array]
UIButton *previousButton;
for (UIButton button in buttons) {
// Buttons take up full width
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"H:|[button]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(button);];
[allConstraints addObjectsFromArray:constraints];
constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"V:|[button]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(button);];
[allConstraints addObjectsFromArray:constraints];
if (!previousButton) {
NSDictionary *metrics = #{#"padding" : #(10)};
// Make buttons height
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[-(padding)-previousButton(==button)]"
options:0
metrics:metrics
views:NSDictionaryOfVariableBindings(previousButton, button)];
[allConstraints addObjectsFromArray:constraints];
}
previousButton = button;
}
[self.view addConstraints:allConstraints]
This doesn't achieve what I need as the buttons don't get pinned to the bottom of the view.
I would do it in a somewhat different way. Rather than building up the constraints inside a loop, I would build the format string in a loop.
-(void)addButtonsWithConstraints:(NSArray *) buttons {
NSMutableDictionary *views = [NSMutableDictionary new];
for (int i = 0; i<buttons.count; i++) {
[buttons[i] setTranslatesAutoresizingMaskIntoConstraints: NO];
[self.view addSubview:buttons[i]];
[views setObject:buttons[i] forKey:[NSString stringWithFormat:#"button%d",i]];
}
NSMutableString *formatString = [#"V:" mutableCopy];
for (int i = 0; i<buttons.count-1; i++) {
[formatString appendFormat:#"[button%d]-10-", i];
}
[formatString appendFormat:#"[button%lu]|", buttons.count - 1]; // pins the last button to the bottom of the view
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:formatString options:NSLayoutFormatAlignAllLeft | NSLayoutFormatAlignAllRight metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[button0]|" options:0 metrics:nil views:views]];
}
The last line sets button0 to the full width of the view, and the format options in the previous line make all the buttons align their left and right edges.
Hi this might help you out
NSArray *buttons = #[button1, button2, button3, button4, button5];
NSMutableArray *allConstraints = [NSMutableArray array];
UIButton *previousButton;
for (UIButton *button in buttons) {
[button setTranslatesAutoresizingMaskIntoConstraints:NO];
// Buttons take up full width
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"H:|[button]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(button)];
[allConstraints addObjectsFromArray:constraints];
if (!previousButton) {
constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"V:|->=10-[button(65)]"
options: 0
metrics:nil
views:NSDictionaryOfVariableBindings(button)];
[allConstraints addObjectsFromArray:constraints];
}
if (previousButton) {
NSDictionary *metrics = #{#"padding" : #(10)};
// Make buttons height
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[previousButton]-(20)-[button(65)]"
options:0
metrics:metrics
views:NSDictionaryOfVariableBindings(previousButton, button)];
[allConstraints addObjectsFromArray:constraints];
}
previousButton = button;
}
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"V:[previousButton]-10-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(previousButton)];
[allConstraints addObjectsFromArray:constraints];
[self.view addConstraints:allConstraints]

Using NSLayoutConstraint Visual Format to set width of items

I'm trying to get a hang of NSLayoutConstraints visual format and I am having a little trouble.
What I want is basically a simple UIView with two labels, one on the left, and one on the right, with heights equal to the height of the container, and the widths equal to 1/2 of the container width. The origin of the right label is aligned to the trailing of the left label.
| LEFT LABEL - RIGHT LABEL|
Anyway, here is my attempt:
self.leftLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.rightLabel.translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary *views = #{ #"leftLabel" : self.leftLabel,
#"rightLabel" : self.rightLabel };
NSDictionary *metrics = #{ #"width" : #(CGRectGetWidth(self.frame) / 2) };
NSArray *constraints;
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[leftLabel]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[rightLabel]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[leftLabel(width)]" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-width-[rightLabel(width)]" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
[self setNeedsUpdateConstraints];
[self updateConstraintsIfNeeded];
Unfortunately, the width of each label remains 0 after this executes. Any suggestions?
UPDATE
Here's the whole class:
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame])
{
self.leftLabel = [UILabel new];
self.rightLabel = [UILabel new];
self.loadingView = [[DDLoadingView alloc] init];
self.rightLabel.backgroundColor = [UIColor redColor];
self.leftLabel.backgroundColor = [UIColor redColor];
self.backgroundColor = [UIColor blackColor];
[self addSubview:self.leftLabel];
[self addSubview:self.rightLabel];
[self configureLayout];
}
return self;
}
- (void)configureLayout
{
self.leftLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.rightLabel.translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary *views = #{ #"leftLabel" : self.leftLabel,
#"rightLabel" : self.rightLabel,
#"loadingView" : self.loadingView };
NSDictionary *metrics = #{ #"width" : #(CGRectGetWidth(self.frame) / 2) };
NSArray *constraints;
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[leftLabel]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[rightLabel]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[leftLabel]-[rightLabel(==leftLabel)]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
[self setNeedsUpdateConstraints];
[self updateConstraintsIfNeeded];
}
You need to set the constraints to use the widths of the other views:
NSArray *constraints;
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[leftLabel]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[rightLabel]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[leftLabel]-[rightLabel(==leftLabel)]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
The last constraint is saying make those two views the same width while pinning them to the left/right egdes of the superview. See the Equal Widths section of the docs
As discussed in the comments, you need to also make sure that the frame of the superview can accommodate the views themselves. |-[ will use the default insets of 20, using |-2- will give insets of 2. If the intrinsic height of the labels is more than the views height minus these insets your view isn't going to show. So you either need to reduce the insets or increase the height of the container.
If you want more information on AutoLayout I recommend the following series of blog posts by #jrturton - see the top of the article for other posts in the series. He's also got a great category on UIView to make adding constraints easier, I use this daily!

Resources