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.
Related
I created an UIView and then added the Anchor constraints but I have a problem when I want to read the values ...
In this case as you see, I created an NSLayoutConstraint property to get the Anchor Width of my uiview ... I then created a CGFloat that contains constraint but my NSLog always returns me a ZERO value.
where am I wrong? how can I get the width values of my UIView assigned to the Anchors?
UIView *trackLine = [[UIView alloc] init];
trackLine.backgroundColor = [self trackLineColor];
trackLine.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:trackLine];
[trackLine.topAnchor constraintEqualToAnchor:mediaTitle.bottomAnchor constant:25].active = YES;
[trackLine.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = YES;
[trackLine.heightAnchor constraintEqualToConstant:1].active = YES;
self.width = [trackLine.widthAnchor constraintEqualToAnchor:self.widthAnchor multiplier:.8];
self.width.active = YES;
CGFloat trackLineLenght = self.width.constant;
NSLog(#"TRACK LINE %f", trackLineLenght );
NSLog Result:
**2017-10-21 17:10:35.096562+0200 [5501:1994879] TRACK LINE 0.000000**
OK - a little confusion...
First, don't use "width" as a property name... very confusing, since width is used in so many places already.
So, let's assume you have:
#property NSLayoutConstraint *trackLineWidthConstraint;
A .widthAnchor doesn't really have a width in terms of "what's the width of the anchor." The way you are defining your constraints:
self.trackLineWidthConstraint = [trackLine.widthAnchor constraintEqualToAnchor:self.widthAnchor multiplier:.8];
Says "set the .trackLineWidthConstraint property to 80% of the width of self. So, whenever the actual width of self changes, the actual width of your trackLine view will change to 80% of the new width.
The .constant is Zero. If it was something other than zero, that value would be added after calculating 80%. For example:
self.trackLineWidthConstraint = [trackLine.widthAnchor constraintEqualToAnchor:self.widthAnchor multiplier:.8];
// if self is 200-pts wide, trackLine will be 160-pts
self.trackLineWidthConstraint.constant = 10
// trackLine width is now (200 * 0.8) + 10, or 170-pts
If you want to get the current width of trackLine, you can get it from its .frame (after auto-layout has finished).
Hopefully, that didn't just make it more confusing :)
Hey I'm trying display a set of "tags" in a view controller using collection view cells but I'm having trouble finding a way to make them be able to dynamically resizable depending on the length of the string.
Right now the individual cells are statically sized so whenever a String that populates the cell with characters exceeding the size of the cell, it goes into the second line. I want it so that the cell can change length depending on the length of the String. So if it's the tag "#Vegan", it will automatically resize so that the tag isn't that big. Likewise, if it's a longer string like "#LaptopFriendly", it will become horizontally longer to accommodate the string and not use the second line. The vertical length can stay fixed. Thank you!
UPDATE (interface builder settings when I run into errors using Rob's code):
Simulator screenshot:
You need unambiguous constraints between your label and the cell (e.g. leading, trailing, top, and bottom constraints):
Then you can use UICollectionViewFlowLayoutAutomaticSize for the itemSize of your collectionViewLayout. Don't forget to set estimatedItemSize, too, which enables automatically resizing cells:
override func viewDidLoad() {
super.viewDidLoad()
let layout = collectionView?.collectionViewLayout as! UICollectionViewFlowLayout
layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
layout.estimatedItemSize = CGSize(width: 100, height: 40)
}
That yields:
You can calculate the lengths of the texts ahead of time, feed them into an array accessible by your collectionView and use them them to construct the size of the cell.
//Create vars
NSArray * texts = #[#"Short",#"Something Long",#"Something Really Long"];
NSMutableArray * lengths = [NSMutableArray new];
float padding = 30.0f;
//Create dummy label
UILabel * label = [UILabel new];
label.frame = CGRectZero;
label.font = [UIFont systemFontOfSize:20.0f weight:UIFontWeightBold];
//loop through the texts
for (NSString * string in texts){
//set text
label.text = string;
//calculate length + add padding
float length = [label.text boundingRectWithSize:label.frame.size
options:NSStringDrawingUsesLineFragmentOrigin
attributes:#{NSFontAttributeName:label.font}
context:nil].size.width + padding;
//save value into array as NSNumber
[lengths addObject:#(length)];
}
//drop label
label = nil;
Create the size of the cell using some code like this:
return CGSizeMake(lengths[indexPath.row], 100.0f);
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];
}
Say i have two labels, close to each ether, and one will maybe grow:
So if the left label will change and grow, i would like the right label to move to the right and give space, but not squeeze, like so:
Normally i just use:
CGFloat width = [self.priceLabel.text sizeWithFont:[UIFont systemFontOfSize:13]].width;
self.myLabel.frame = CGRectMake(self.myLabel.frame.origin.x, self.myLabel.frame.origin.y, width,self.myLabel.frame.size.height);
and move the right label to the end of of the left label,
But i'm using AutoLayout and looking for a way to make it possible
Thanks!!
You can start by trying the visual format:
NSString *visualFormat = #"|-[label1]-[label2]";
NSLayoutFormatOptions options = NSLayoutFormatAlignAllCenterY | NSLayoutFormatDirectionLeftToRight;
NSDictionary *views = NSDictionaryOfVariableBindings(label1, label2);
NSArray *layoutConstraints = [NSLayoutConstraint constraintsWithVisualFormat:visualFormat options:options metrics:nil views:views];
[view addConstraints:layoutConstraints];
If you also want to add a right margin and any available space in the middle you can use
NSString *visualFormat = #"|-[label1]-#1-[label2]-|";
Check the visual format guide for all possible options.
So I have setup a UIView that contains a UIScrollView (and child content view) that has sub views that are series of UILabels and UIViews that grow and shrink depending on the content contained in them, all using AutoLayout from the Storyboard. This works when I have something like Label - Label - Label - View w/o any issues, however if I put an empty UIView in-between two labels and insert sub views on the UIView, I'm not seeing the results I'm expecting. I have the following layout in a storyboard:
...where the teal and blue views are labels that grow to infinite height and the orange view (optionsPanel) is an empty UIVIew that I later inject sub views into. The rest of the stuff on the window is UILabels and UISegment controls. Between each row of views I have a Vertical Space constraint with a constant of 8. This all worked beautifully until I had to put in the empty UIView and programmatically inject sub views. The code I would expect to work would be something like (optionsPanel is the orange colored UIView)...
optionsPanel.translatesAutoresizingMaskIntoConstraints = YES;
NSArray *options = [product objectForKey:#"options"];
lastTop = 10;
for(int i=0;i<options.count; i++) {
NSDictionary *option = [options objectAtIndex:i];
NSArray *values = [option objectForKey:#"values"];
if([self hasNoneValue:values] && values.count == 2) {
NSDictionary *value = [self notNoneValue:values];
M13Checkbox *optionCheck = [[M13Checkbox alloc] initWithTitle:[option objectForKey:#"name"]];
optionCheck.frame = CGRectMake(0, lastTop, 280, 25);
[optionsPanel addSubview:optionCheck];
lastTop += 25;
} else {}
}
...where the orange UIView would magically grow and everything would just get pushed around accordingly, however this is what I'm seeing:
...the orange UIView does not grow at all, and the other two top UIView have gone somewhere off the screen. So my next guess was to turn off the Autoresizing Mask using...
optionsPanel.translatesAutoresizingMaskIntoConstraints = NO;
...but I'm getting a result where everything appears to be working but the orange UIView (optionsPanel) has no height for whatever reason and looks like:
This is getting closer to what I would expect, so I thought I would force the height of the orange UIView using code like...
frame = optionsPanel.frame;
frame.size.height = lastTop;
optionsPanel.frame = frame;
...but this appears to have no affect on anything.
Purely guessing, I found that this code almost works, if I arbitrary set the optionPanel's origin to something much larger than the space that is needed....
optionsPanel.translatesAutoresizingMaskIntoConstraints = YES;
NSArray *options = [product objectForKey:#"options"];
lastTop = 10;
for(int i=0;i<options.count; i++) {
NSDictionary *option = [options objectAtIndex:i];
NSArray *values = [option objectForKey:#"values"];
if([self hasNoneValue:values] && values.count == 2) {
NSDictionary *value = [self notNoneValue:values];
M13Checkbox *optionCheck = [[M13Checkbox alloc] initWithTitle:[option objectForKey:#"name"]];
optionCheck.frame = CGRectMake(0, lastTop, 280, 25);
[optionsPanel addSubview:optionCheck];
lastTop += 25;
} else {}
}
lastTop += 10;
frame = optionsPanel.frame;
frame.size.height = lastTop;
frame.origin.y += 300; //some arbitrarily large number
optionsPanel.frame = frame;
..which gives this result:
...but apparently the AutoLayout has decided that the name label needs to take up the extra space. Its an ugly approach but if I could figure out how much space I need then I could just push everything down, if I had to. What's the secret to having a dynamic UIView between two dynamically sized labels and everything just work???
As #Timothy says, you need to manually add constraints to the subviews of the orange view if you want it to resize based on its contents—views don’t do this by default.
In general, if you’re using autolayout in a window, you should never be manually setting the frame of any view. Autolayout overrides any frames you set the every time it’s called, so even if you manage to manually get it working for a second it’ll fail the next time anything triggers a layout.
For views created in code, it's perfectly fine to set their frames as long as their translatesAutoresizingMaskIntoConstraints property is YES (the default, by the way).
However, for a view instantiated in storyboard or a nib, you can not set its translatesAutoresizingMaskIntoConstraints to YES.