Why is NSLayoutAttributeCenterX centering around 0 instead of container view center? - ios

I am having a problem with NSLayoutAttributeCenterX that i can't figure out.
I created a NIB for a custom view and within this custom view I inserted a UILabel that I want to center in the x axis. And I am then inserting this custom view in my root ViewController's view.
Now, I overrided the "layoutSubviews" method of the custom view to put my constraints there to center the Label within the custom view.
I called the UILable inside this view "appTitle".
Below is the content of the "layoutSubviews" method of my custom view. As you can see there, I used the following constraint method to center the UILabel at the center of the custom view, in the x axis.
NSLayoutConstraint *titleConstraint_H = [NSLayoutConstraint constraintWithItem:self.appTitle attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1 constant:0];
But, the end result is that the UILabel is centered around x= 0 , instead of x = 160, which is the center of the custom view that contains it. I do not understand why this is happening.
In my simulation I am using an ipHone 5s, in PORTRAIT MODE , and I am setting the width of the custom view to be equal to the width of the phone's frame , which is 320. Thus x = 160 is the x coordinate of the center point.
Below is the code inside the "layoutSubviews"
LLDB output that shows the center of the appTitle label after the constraint has been enacted (po (CGPoint) [self.appTitle center])
LLDB output that shows the center of the custom view that contains the label "appTitle" (po (CGPoint)[self center])
and the final output
I wonder if someone has run into this problem too and has figured it out
- (void)layoutSubviews {
//######### TITLE ####################################
UIFont *font = [UIFont fontWithName:#"Avenir-Black" size:20.0];
UIColor *color = [UIColor blackColor];
NSDictionary *attrsDictionary = [NSDictionary dictionaryWithObjects:#[font,color] forKeys:#[NSFontAttributeName,NSForegroundColorAttributeName]];
NSAttributedString *titleString = [[NSAttributedString alloc] initWithString:#"WHINE-O-METER" attributes: attrsDictionary];
self.appTitle.attributedText = titleString;
// Adjust UILabel frame to size of text
[self.appTitle sizeToFit];
// Center title on horizontal length
NSLayoutConstraint *titleConstraint_H = [NSLayoutConstraint constraintWithItem:self.appTitle attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1 constant:0];
NSDictionary *viewsDictionary = #{ #"title":self.appTitle};
NSArray *titleConstraint_V = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-20-[title]-20-|" options:0 metrics:nil views:viewsDictionary];
[self addConstraints:titleConstraint_V];
[self addConstraint:titleConstraint_H];
}
thanks!

That's a lot of code to do something that can be done much more easily in your xib file. My recommendation is to remove all that code and set this up in your xib using auto layout. If you are using auto layout, don't call sizeToFit, just set leading and trailing constraints on the label that contains your text. You should not need to implement layoutSubviews to achieve this.

You should not be adding or modifying constraints in -layoutSubviews. From the docs:
You should override this method only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want.
Rather, you should set up constraints in an override of -updateConstraints. And don't forget to call through to super when you override methods like this. (That may actually contribute to your problem, too. Your override of -layoutSubviews does not call through to super.)

Related

iOS initialise view programmatically without constraining the position

Here is my problem, I have a scroll view scrollExerciseIndex that I use only as a scrolling bar, in this scroll view I place a UIView indexesView and I want it to be always at the center of the scroll view. For this I use layout constraints :
UIView * indexesView = [[UIView alloc] initWithFrame: CGRectMake(xPosition, 0, dimension*numberIndexes, dimension)];
[self.scrollExerciseIndex addSubview:indexesView];
[self.scrollExerciseIndex setContentSize:CGSizeMake(dimension*numberIndexes, dimension)];
if (xPosition != 0) {
NSLayoutConstraint * xCenterConstraint = [NSLayoutConstraint constraintWithItem:indexesView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.scrollExerciseIndex attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0];
[self.scrollExerciseIndex addConstraint:xCenterConstraint];
}
Here is the expected result :
Don't pay attention to all the element, just the bar at the bottom of the screen is my problem.
I have to create view programmatically because sometimes I will activate the constraints, sometimes not and I have to set the frame of the view dynamically. So for now I initialise the view indexesView like so :
UIView * indexesView = [[UIView alloc] initWithFrame: CGRectMake(xPosition, 0, dimension*numberIndexes, dimension)];
(I know, not very original)
I would like to know if there is a way to initialize the view programmatically but to say to auto-layout that it has no constraints on the position because right now if the screen turns in landscape mode there is a conflict as the scrollview's frame changes so the distance between the center of the scroll view (on which I set a constraint) and the position of the subview's frame (xPosition) is no longer the same.
As you can see, the view is no longer at the center of the scroll view and I have some constraints broken.
Will attempt to recover by breaking constraint
NSLayoutConstraint:0x7bed6c50 UIView:0x7bed6ad0.centerX == UIScrollView:0x7e273200.centerX
Thanks for your help.
Ok, I found what I was looking for by reading a book about Audio-Layout.
My problem was that audio layout would create constraints behind my back automatically. When using AutoLayout a type of constraints is created from non-autoLayout specifications (The used to describe interface when auto layout didn't exist). So constraints are created using the initial frame of the view. The only thing I had to do was :
[indexesView setTranslatesAutoresizingMaskIntoConstraints:NO];
to disable this creation of constraints from the frame, and then recreate explicitly the constraints for width and height if needed (which wasn't the case for me, but I still made the test) like so :
`NSLayoutConstraint * widthConstraint = [NSLayoutConstraint constraintWithItem:indexesView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:1.0 constant:widthValue];
NSLayoutConstraint * heightConstraint = [NSLayoutConstraint constraintWithItem:indexesView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:1.0 constant:heightValue];
[indexesView addConstraint: heightConstraint];
[indexesView addConstraint: widthConstraint];`
When adding constraints programmatically, don't forget to call : [indexesView setNeedsUpdateConstraints]; so the constraints are recalculated only when needed.
Last info that I read and can be useful in general, when adding a lot of constraints, the apple doc specifies that it is more efficient to use the method :
[myView addConstraints:(NSArray<NSLayoutConstraints *> *)] than to call addConstraint: for each constraint.
Hope it can be useful to someone.

NSLayoutConstraint programmatically set view frame

I know there is are similar topics, but i cant figure out how to do simple thing, for example, set view frame programmatically with Auto Layout constraints. Its pretty easy to do with Storyboard, but when i try to learn about NSLayourConstraint i realise that i'm not understand topic.
Consider that we have UIView and 3 buttons. First button at top have 3 constraints (leading and trailing, and to top of a view). Other 2 buttons centered horizontally with that button and have equal widths. its pretty easy layout, i upload screenshot:
I have read about visual format language, but what i cant understand is - how to create constraint, for example, that relay to top (or trailing-leading)? Like following:
Task look pretty simple but still, i did not found a way how to do that programmatically with NSLayoutConstraint. Could you please provide a solution? Thanks.
Here's a solution (should go in -viewDidLoad). There are a a couple of things to note:
Firstly, VFL doesn't allow you to create all possible types of constraint. In particular, centering needs to be done with the +constraintWithItem: class method on NSLayoutConstraint.
Secondly, as noted in the comments, you could just use hardcoded left and right pad values in the horizontal VFL string to achieve the centering, but this might cause problems if you need to support different device sizes.
Thirdly, the call to -setTranslatesAutoresizingMaskIntoConstraints: is critical. Programmatic Autolayout will completely fail to work if you forget this. Also you need ensure all views are added to their superviews before setting up constraints, otherwise any constraint string referencing a superview will cause a crash
NSArray *names = #[#"button1",#"button2",#"button3"];
NSMutableDictionary *views = [[NSMutableDictionary alloc]init];
for(NSUInteger i=0;i<3;i++) {
UIButton *b = [[UIButton alloc]init];
NSString *name = names[i];
[b setTitle:name forState:UIControlStateNormal];
views[name] = b;
[b setBackgroundColor:[UIColor redColor]];
[b setTranslatesAutoresizingMaskIntoConstraints:false];
[self.view addSubview:b];
}
//List of values to be used in the constraints
NSDictionary *metrics = #{
#"buttonWidth":#150,
#"bHeight":#50, //button height
#"topPad":#100,
#"vPad":#20 //vertical padding
};
//Horizontal VFL string (repeated for all rows).
for (NSString *buttonName in views.allKeys) {
NSString *horizontalConstraintString = [NSString stringWithFormat:#"|-(>=0)-[%#(buttonWidth)]-(>=0)-|",buttonName];
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:horizontalConstraintString options:0 metrics:metrics views:views];
[self.view addConstraints:horizontalConstraints];
//Can't do centering with VFL - have to use constructor instead. You could also hardcode left and right padding in the VFL string above, but this will make it harder to deal with different screen sizes
NSLayoutConstraint *centerConstraint = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:views[buttonName] attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0];
[self.view addConstraint:centerConstraint];
}
//Vertical VFL (vertical spacing of all buttons)
NSString *verticalConstraintString = #"V:|-topPad-[button1(bHeight)]-vPad-[button2(bHeight)]-vPad-[button3(bHeight)]-(>=0)-|";
NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:verticalConstraintString options:0 metrics:metrics views:views];
[self.view addConstraints:verticalConstraints];
[self.view layoutIfNeeded];

How to stick UIView to the right side of it's superview programmatically

I am desperately trying to stick one of my UILabels to the right edge of it's superview while the label's width is variable (it's a time so the thing is getting bigger and should be expanding to the left, this is done using sizeToFit inside of the label when text is set).
So far I have tried loads of things but closest I got with:
_elapsedTimeRightConstraint = [NSLayoutConstraint constraintWithItem:_elapsedTimeView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1.0 constant:-150];
While the label is initially set to 150px width. But when I modify the constant, it all goes to hell.
_elapsedTimeRightConstraint.constant = (_elapsedTimeView.frame.size.width * -1);
[self layoutIfNeeded];
So my question is, how do I align trailing edges of a view and it's superview (so it sticks to the right) when the width of the subview is constantly changing. I have been using FLKAutoLayout elsewhere in the project so if this can be done this framework easily than great, but basic autolayout solution would be amazing too!!!
First, make sure that translatesAutoresizingMaskIntoConstraints is set to NO, if you are creating the label programmatically.
The first constraint you need is "label.trailing = superview.trailing".
[NSLayoutConstraint constraintWithItem:label
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeTrailing
multiplier:1.f
constant:0.f]
This will pin the right edge (on left-to-right languages) of the label on the right edge of the superview.
You will now need a constraint for the Y position.
In my test, I have vertically centred the label with the following constraint:
[NSLayoutConstraint constraintWithItem:label
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeCenterY
multiplier:1.f
constant:0.f]
Now comes the trick!
Every time you change the text on the label, you need to recalculate the frames with AutoLayout.
[superview setNeedsLayout];
[superview layoutIfNeeded];
AutoLayout will:
1) Ask the label of its new size (based on its text).
2) Adjust the size of the label.
3) Pin the trailing edge of the label to the trailing edge of the superview.
Further research
The issue with UILabel is that when you're using AutoLayout and you set text, its intrinsicContentSize changes, but it doesn't trigger a layout update.
A way to enforce this without subclassing UILabel is to use Objective-C runtime.
#interface UILabel (AutoLayout)
- (void)swz_setText:(NSString*)text;
#end
#implementation UILabel (AutoLayout)
+ (void)load
{
NSLog(#"Swizzling [UILabel setFont:]...");
Method oldMethod = class_getInstanceMethod(self, #selector(setText:));
Method newMethod = class_getInstanceMethod(self, #selector(swz_setText:));
method_exchangeImplementations(oldMethod, newMethod);
}
- (void)swz_setText:(NSString*)text
{
if (![text isEqualToString:self.text]) {
[self setNeedsLayout];
}
[self swz_setText:text]; //This now points to "setText:" - not a mistake!
}
#end
In this category, I'm "enhancing" setText: implementation by calling setNeedsLayout if the text changes.
Now you just need to invoke layoutIfNeeded on the superview to recalculate/realign the label frame.
Click here for the playground (Swift 2.0 - Xcode 7) where I've tested my code.
I hope this helps.
Hello here are some points to achieve what you want:
Set NSLayoutConstraintTrailing constant equal to 0 and the leading constraint NSLayoutAttributeLeading set it as greater than or equal the value you want.
Use NSLayoutConstraintTrailing and NSLayoutAttributeLeading instead of right and left to handle other languages
I hope this helps
[yourLabel sizeToFit];
CGRect frame = yourLabel.frame;
frame.x = parentView.frame.size.width - yourLabel.frame.size.width;
yourLabel.frame = frame;
It completely ignores the "best practice" of using the autolayout features, but if you just can't stand it anymore....that should work. ;-)

Resizing UIViews on click of a button using Autolayout

I am new to Auto layout constraints. I have 2 views(topView and paintView) on my main view, along with a button on the top right corner of the main view. On loading the view, the topView occupies the whole main view(excluding the button). On click of the button, I want the topView to occupy 70% of the main view and the paintView to occupy the rest(excluding the button).
I have set up the the X, Y and top constraints for the topView using storyboard. The paintView and the corresponding constraints have been set up programmatically.
The code I have now is this:
-(void)setupPaintView
{
UIView *pPaintView = [UIView new];
[pPaintView setBackgroundColor:[UIColor yellowColor]];
pPaintView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:pPaintView];
self.paintView = pPaintView;
[self addConstraintsToView];
//[self setTopViewFrame];
}
-(void)addConstraintsToView
{
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.paintView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.topView attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0]];
[self.view addConstraint:[NSLayoutConstraint
constraintWithItem:self.paintView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.topView
attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0.0]];
[self.view addConstraint:[NSLayoutConstraint
constraintWithItem:self.topView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.paintView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0.0]];
NSLayoutConstraint *pHeightConstraintTopView = [NSLayoutConstraint
constraintWithItem:self.topView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeHeight
multiplier:1.0
constant:0.0];
self.heightconstraintTopView = pHeightConstraintTopView;
[self.view addConstraint:pHeightConstraintTopView];
NSLayoutConstraint *pHeightConstraintPaintView = [NSLayoutConstraint
constraintWithItem:self.paintView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeHeight
multiplier:0.0
constant:0.0];
self.heightconstraintPaintView = pHeightConstraintPaintView;
[self.view addConstraint:pHeightConstraintPaintView];
}
On button click the following method gets called:
-(IBAction)detailBtnClick:(id)sender
{
if(self.heightconstraintPaintView.constant == 0)
{
self.heightconstraintTopView.constant = 0.7*self.view.frame.size.height;
self.heightconstraintPaintView.constant = 0.3*self.view.frame.size.height;
[self.view setNeedsUpdateConstraints];
}
else
{
self.heightconstraintTopView.constant = self.view.frame.size.height;
self.heightconstraintPaintView.constant = 0;
[self.view setNeedsUpdateConstraints];
}
}
When the view loads, the topView acquires the main view's height, which is desired here. But when I click on the button, the topView remains at 100% i.e. it does not resize and neither does the paintView. I am modifying the constant property of the topView and the paintView constraints, but I am not sure that is the correct way to go about it. The constraint here is that the views have to be laid out using Autolayout constraints only. How can I get the views to resize at the click of the button?
Any help is welcome.
Thanks to timothykc and others, I have successfully navigated the problem stated above. But I am facing another issue now.When I change the orientation of the simulator to landscape, the paintView remains almost hidden. Following is the code (toggle is a boolean value that decides whether to stretch/shrink the views):
-(IBAction)detailBtnClick:(id)sender
{
if(self.toggle == FALSE)
{
self.topViewHeightConstraint.constant = 0.7*self.bounds.frame.size.height;
self.heightconstraintPaintView.constant = 0.3*self.bounds.frame.size.height;
//[self.view layoutIfNeeded];
}
else
{
self.topViewHeightConstraint.constant = self.view.bounds.size.height;
self.heightconstraintPaintView.constant = 0;
//[self.view layoutIfNeeded];
}
self.toggle = !self.toggle;
}
The topViewHeightConstraint has been added as a property as indicated by timothykc. This is working properly for the portrait orientation, but is not working properly for landscape, as the height of the topView does not change as desired(70%), meaning that the ratios are not getting handled properly.
I'm going to provide a storyboard driven solution that should help you with other autolayout problems down the road.
My solution to your specific problem, you've got two views (1 and 2 in diagram below):
For view 1, pin the view to the left, top, and right of the superview. Then set a height constant. (e.g. 568, the full height of an iphone 5s)
For view 2, pin it to the left, bottom, and right of the superview. Then pin it to the bottom of view 1.
Open up the assistant editor view, and here's the key trick--turn the height constraint on view 1 into a nslayoutconstraint property on your VC. You do this by locating the constraint, and then control-dragging onto the VC. (e.g.`
#property (strong, nonatomic) IBOutlet NSLayoutConstraint *viewHeight;`
Now you can manipulate this property with an action linked to your button, such as
- (IBAction)scale:(id)sender {
self.viewHeight.constant = 397.6; //%70 of 568....
}
In my example, I change the nslayoutconstraint.CONSTANT manually to an arbitrary value.
To understand what's happening, you need to know that autolayout is a means for determining the (x coord,y coord,width, height) of any layout object. Warnings occur when xcode cannot ascertain all 4 values...
In View 1, we give a constraint for Height. X,Y, and Width are extrapolated from the distance to the superview. (if something is 0 from the left and right, then the width fills the whole screen...; if 0 from top and left, then coords must be (0,0))
In view 2, X must be 0 since distance from left is 0. width whole screen... Height and Y are extrapolated based on the height of View 1!
So when we mess with height constraint in View 1, it effects the height and Y coord of View 2!
To get constraints to update on a view you would need to call [self.view layoutIfNeeded]; instead of [self.view setNeedsUpdateConstraints]; after setting the new constant on whichever constraint(s) you would like to update.
Actually this is more of an comment about my methods, but I decided to post it as an answer because firstly, this has solved my problem and secondly, it involves some snippets of code which is hard to read in the comments section. Regarding the orientation problem mentioned in the edit, I came up with a workaround to accommodate the view reszing requirements with respect to the toggle button and with respect to orientation change. The three methods used for this purpose are:
The following method is called on the button click event.
-(IBAction)detailBtnClick:(id)sender
{
[self updateViewConstraints:self.toggle];
self.toggle = !self.toggle;
}
The following method updates the constraints.
-(void)updateViewConstraints :(BOOL)toggleValue
{
if(toggleValue == FALSE)
{
self.topViewHeightConstraint.constant = 0.7*self.view.bounds.size.height;
self.heightconstraintPaintView.constant = 0.3*self.view.bounds.size.height;
}
else
{
self.topViewHeightConstraint.constant = self.view.bounds.size.height;
self.heightconstraintPaintView.constant = 0;
}
}
The following method calls the method above to update constraints in case of orientation change:
-(void)viewWillLayoutSubviews
{
[self updateViewConstraints:!self.toggle];
}

Constraints not working with a UITextView

I have a View controller, in which the view has two image views and two text views. I turned off auto layout, and I programmatically set the distance between the first text view and the first image view by using this code:
The following code is in the viewDidLoad method of my custom view controller class. I have set the autoresizing mask to no in both cases, so I have no idea why the code doesn't work.
(tf2_logo is the image view and itemName is the text view)
self.tf2_logo.translatesAutoresizingMaskIntoConstraints = NO;
[self.backpackBackground addConstraint:[NSLayoutConstraint constraintWithItem:self.itemName attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.tf2_logo attribute:NSLayoutAttributeTop multiplier:1.0 constant:-1.0]];
[self.backpackBackground addConstraint:[NSLayoutConstraint constraintWithItem:self.tf2_logo attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.backpackBackground attribute:NSLayoutAttributeLeft multiplier:1.0 constant:17]];
Now I want to do the same thing with my other text view, basically I wanted to keep the distance between the itemName text view and the text view at a certain distance. I used this code:
(tf2 is my other text view)
self.tf2.translatesAutoresizingMaskIntoConstraints = NO;
[self.backpackBackground addConstraint:[NSLayoutConstraint constraintWithItem:self.itemName attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.tf2 attribute:NSLayoutAttributeTop multiplier:1.0 constant:-3.0]];
[self.backpackBackground addConstraint:[NSLayoutConstraint constraintWithItem:self.tf2 attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.tf2_logo attribute:NSLayoutAttributeRight multiplier:1.0 constant:20]];
After implementing this code, the tf2 text view doesn't even show up in the view controller. What is the problem?
EDIT: You can download the whole project here: https://www.dropbox.com/sh/u820u2ndyrncuz8/P4atI-9CAx
EDIT#2:
You mentioned that you turned off auto layout, because UITextView has that little gap on top in iOS7. To remove the gap, try this:
self.tf1.textContainerInset = UIEdgeInsetsZero;
When you log the original value of the textContainerInset it shows: {8, 0, 8, 0} . The two 8's are responsible for the gap (one at the top). The line above sets all values to zero and the content is nicely aligned to the top of the frame.
(EDIT#1: Completely changed the answer)
I assume you primarily want to have a flexible height of the imageName UITextView. First I suggest to use auto layout. You can set constraints in Xcode according to the following image:
The red lines are the constraints. The green line is special: It shall be a height constraint and you create an outlet for it in the view controller. (Open the document outline view, locate the height constraint in the tree and control-drag it to the code.)
Then in the viewDidLoad method:
CGSize size = [self.tf1 sizeThatFits:self.tf1.frame.size];
self.tf1Height.constant = size.height;
The height of the "lore ipsum" field now adjusts to its content.
Have you tried using frames instead of constraints? If your not using autolayout I think frames might be easier to read/implement.
sample:
// tf2 will be placed at (0,0) in superview and have width of 100 and height of 20
tf2.frame = CGRectMake(0, 0, 100, 20);
you can play around with different values to get your layout as desired.

Resources