How to add constraints to items with random positions - ios

I have an ios app that I add a variable number (between 2 and 10) labels to in randomly generated positions. It's all done programmatically. This is how the location of the labels is determined.
int width = self.view.frame.size.width - 200;
int height = self.view.frame.size.height - 200;
newFrame.origin.x = arc4random() % width;
newFrame.origin.y = 80 + arc4random() % (height-80);
All of the labels are added to an array, self.viewLabels, after they are created and added to the view, otherwise there's no permanent reference to them because they are created in a loop
while (numViews < (numLabels)){
CustomLabel *timer = [[CustomLabel alloc] init];;
....
It works fine, except when I turn the app to landscape view. Some of the labels disappear that were at the bottom of the portrait view. I'm looking into adding constraints programmatically, and I understand the first step is to add the elements that need to be constrained to this dictionary
NSDictionary *views = NSDictionaryOfVariableBindings(button, button2);
Since I only have reference to these labels in the array self.viewLabels, I'm trying to figure out if there's a way I can get the labels in that dictionary. I tried to use the iterator to create unique names for the labels
for (int i = 0; i < [self.viewLabels count]; i++){
CustomLabel * label[i] = self.viewLabels[i];
}
That doesn't work, and even if it did, I can't figure out how to add them to the dictionary. And even if I got them in a dictionary, how to add constraints to items that have random positions in the view?
Can you suggest a strategy I could use in this situation?
Update
If it's impossible to add constraints after I've randomly generated positions, is it possible to do something when I create the positions to ensure they will all be visible in both landscape and portrait?
Update 2- based on the first answer by #rdelmar, I've tried the code below (i.e. adding labels without frames and then adding constraints after they are added to the view). However none of the labels are appearing on screen. You can see how my code was before by the lines I've commented out. I had previously added labels in random locations...
while (numViews < (numLabels)){
CustomLabel *label = [[CustomLabel alloc] init];;
//
// label.frame = CGRectMake(0, 0, 150, 50); //removed the frame
label.text = #"blah";
// newFrame = label.frame;
// int width = self.view.frame.size.width - 200;
// int height = self.view.frame.size.height - 200;
// newFrame.origin.x = arc4random() % width;
// newFrame.origin.y = 80 + arc4random() % (height-80);
// label.frame = newFrame;
[label setFont:[UIFont systemFontOfSize:50]];
UITapGestureRecognizer *tgr = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(handleGesture:)];
tgr.numberOfTapsRequired = 1;
tgr.numberOfTouchesRequired = 1;
[label addGestureRecognizer:tgr];
label.userInteractionEnabled = YES;
[label setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:label];
[self.gameClocks addObject: label];
numViews += 1;
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:label
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:1
constant:0]];
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:label
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeHeight
multiplier:1
constant:0]];

Try thinking about this in a different way -- you don't add constraints to views with randomly generated positions, you create random constraints that result in the views having random positions. So, when you create the views, you don't give them any frame. You create the label, add it to the subview, then add the constraints. If you want the labels to be visible in both portrait and landscape, it would be best to use the multiplier rather than the constant values of the constraints so the position is relative to the size of the view (not a constant distance from some edge). To do this, you would use constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:, rather than the visual format language, so you don't have to worry about the views dictionary. When you use the multiplier, you have to use the right edge or the bottom edge of the superview, since those have non-zero values (while the top and left side do not).
After Edit:
This is one way to do it. I create random locations by passing in a random number between 0 and 1 to the multiplier coefficient. To keep the labels inside the view, I pin either the label's left side or right side depending on whether the multiplier value would result in the label being close to the left side or right side of the superview (same with top or bottom). I am also making the height and width of the label relative to the size of the view, so the labels are shorter but wider in landscape.
#interface ViewController ()
#property (strong,nonatomic) NSMutableArray *labelArray;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.labelArray = [NSMutableArray new];
while (self.labelArray.count <10) {
UILabel *label = [UILabel new];
label.backgroundColor = [UIColor orangeColor];
[self.view addSubview:label];
[self.labelArray addObject:label];
[self createConstraintsForRanomPositions:label];
}
for (int i = 0; i< self.labelArray.count; i++) {
[self.labelArray[i] setText:[NSString stringWithFormat:#"Label %d", i]];
}
}
-(void)createConstraintsForRanomPositions:(UIView *) view {
[view setTranslatesAutoresizingMaskIntoConstraints:NO];
CGFloat rightMultiplier = arc4random_uniform(100)/ 100.0;
CGFloat bottomMultiplier = arc4random_uniform(100)/ 100.0;
NSLayoutConstraint *con1;
if (bottomMultiplier <= .2) {
con1 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:0 toItem:self.view attribute:NSLayoutAttributeBottom multiplier:bottomMultiplier constant:0];
}else{
con1 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom relatedBy:0 toItem:self.view attribute:NSLayoutAttributeBottom multiplier:bottomMultiplier constant:0];
}
NSLayoutConstraint *con2;
if (rightMultiplier <= .2) {
con2 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeft relatedBy:0 toItem:self.view attribute:NSLayoutAttributeRight multiplier:rightMultiplier constant:0];
}else{
con2 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeRight relatedBy:0 toItem:self.view attribute:NSLayoutAttributeRight multiplier:rightMultiplier constant:0];
}
NSLayoutConstraint *con3 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeWidth relatedBy:0 toItem:self.view attribute:NSLayoutAttributeWidth multiplier:.2 constant:0];
NSLayoutConstraint *con4 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeHeight relatedBy:0 toItem:self.view attribute:NSLayoutAttributeHeight multiplier:.1 constant:0];
[self.view addConstraints:#[con1, con2, con3, con4]];
[self.view layoutIfNeeded]; // this is needed, otherwise the frames are all {{0,0}, {0,0}} in the following forloop
for (UIView *placedView in self.labelArray) { // rejects any label that overlaps with any other
if (![placedView isEqual:view] && CGRectIntersectsRect(CGRectInset(view.frame, -2, -2), placedView.frame)) {
[view removeFromSuperview];
[self.labelArray removeObject:view];
break;
}
}
}

Related

NSLayoutConstraint in a Loop FOR

I'm trying to create UILabel through a for loop that gets its NSInteger from an NSArray. The cycle works properly but I can not see all the vertices one below the other. I'm definitely wrong something with constraint. Can you help me?
Based on this code I posted how can I fix the NSLayoutConstraint problem within a for loop?
NSInteger max = [[_yValueArray valueForKeyPath:#"#max.self"] integerValue];
NSInteger min = [[_yValueArray valueForKeyPath:#"#min.self"] integerValue];
for (NSInteger value = max; value >= min; value -=1) {
UILabel *valueLabel = [[UILabel alloc] init];
valueLabel.textColor = [UIColor lightGrayColor];
valueLabel.backgroundColor = [UIColor redColor];
valueLabel.textAlignment = NSTextAlignmentLeft;
valueLabel.font = [UIFont defaultAppFontWithSize:10];
valueLabel.translatesAutoresizingMaskIntoConstraints = NO;
valueLabel.text = [self yValueString:value];
valueLabel.tag = value;
[yAxisView addSubview:valueLabel];
[yAxisView addConstraint:[NSLayoutConstraint constraintWithItem:valueLabel attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:valueLine attribute:NSLayoutAttributeLeft multiplier:1 constant:-2]];
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:valueLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:yAxisView attribute:NSLayoutAttributeTop multiplier:1 constant:0];
[yAxisView addConstraint:topConstraint];
NSLayoutConstraint *height = [NSLayoutConstraint constraintWithItem:valueLabel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:valueLine attribute:NSLayoutAttributeHeight multiplier:1 constant:10];
[yAxisView addConstraint: height];
// topConstraint.constant += height.constant;
}
What I'm trying to get is a bar graph like this ... At this time I wanted to get the result of the Y axis you see in the image ...
I'm gathering that the question is labeling the left axis vertically.
If your goal is to add labels to the left edge, you can set the centerY constraint as a multiple of the centerY of its superview. The only trick is that it doesn't like a 0 for the multiplier for centerY, so for that top one, I'd just set the centerY to be equal to the top of its superview. Thus:
NSInteger max = 10;
for (NSInteger i = 0; i <= max; i++) {
UILabel *label = [[UILabel alloc] init];
label.translatesAutoresizingMaskIntoConstraints = false;
label.text = [NSString stringWithFormat:#"%ld", (long)i];
[self.chartDataView addSubview:label];
CGFloat multiplier = (CGFloat)(max - i) * 2.0 / (CGFloat)max;
[self.chartDataView addConstraint:[NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.chartDataView attribute:NSLayoutAttributeLeft multiplier:1 constant:-5]];
if (multiplier == 0) {
[self.chartDataView addConstraint:[NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.chartDataView attribute:NSLayoutAttributeTop multiplier:1 constant:0]];
} else {
[self.chartDataView addConstraint:[NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.chartDataView attribute:NSLayoutAttributeCenterY multiplier:multiplier constant:0]];
}
}
Yields:
Clearly, I'm not drawing any chart, but this shows how, given a chart graph area, you can use the multiplier to dictate where the labels go.

How to create views as a grid(such as 9*9 grid, 15*15 grid) by using for loops in Objective-C

I am trying to draw a 15*15 grid in Objective-C. The grid colour is blue which is like making the board of the "Snake" game in NOKIA.
I've tried using the for loop to create subviews but it seems not working, I looked over the internet and most of them are using UIImageView to make the grid, is there any solution for making a 15*15 grid with for loop or I have to use the UIImageView method?
- (void)viewDidLoad {
[super viewDidLoad];
int i;
int row = 0;
int col = 0;
int firstColor=0;
if ([[UIDevice currentDevice] userInterfaceIdiom]== UIUserInterfaceIdiomPhone){
cellWidth = 23;
CellHeight= cellWidth;
topY=230-4*CellHeight;
leftX=110-4*cellWidth;
}else{
cellWidth=46;
CellHeight=cellWidth;
topY=500-4*CellHeight;
leftX=384-4*cellWidth;
}
UIView *viewA = [[UIView alloc]initWithFrame:CGRectMake(0, 30, 414, 434)];
viewA.backgroundColor = [UIColor blackColor];
[self.view addSubview:viewA];
for (int row = 0; row < rows; row = row++ ){
for(int r = 0; r <= 414; r = r + 23){
UIView *viewB = [[UIView alloc]initWithFrame:CGRectMake(r,5,23,23)];
viewB.backgroundColor = [UIColor blueColor];
[viewA addSubview:viewB];}}
col=col+1;
firstColor=1-firstColor;
if (col>18){
row=row+1;
firstColor=1-firstColor;
col=0;
}
If using iOS 9, the stacked view handles laying out a grid of view nicely:
NSInteger n = 15;
UIStackView *rowView = [[UIStackView alloc] init];
rowView.translatesAutoresizingMaskIntoConstraints = false;
rowView.axis = UILayoutConstraintAxisHorizontal;
rowView.distribution = UIStackViewDistributionFillEqually;
[self.view addSubview:rowView];
for (NSInteger row = 0; row < n; row++) {
UIStackView *columnView = [[UIStackView alloc] init];
columnView.translatesAutoresizingMaskIntoConstraints = false;
columnView.axis = UILayoutConstraintAxisVertical;
columnView.distribution = UIStackViewDistributionFillEqually;
[rowView addArrangedSubview:columnView];
for (NSInteger col = 0; col < n; col++) {
UIView *view = [[UIView alloc] init];
view.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256) / 255.0
green:arc4random_uniform(256) / 255.0
blue:arc4random_uniform(256) / 255.0
alpha:1.0];
view.translatesAutoresizingMaskIntoConstraints = false;
view.layer.borderColor = [UIColor blackColor].CGColor;
view.layer.borderWidth = 0.5;
[columnView addArrangedSubview:view];
// make it it the width of the column stack view, and 1/n of the height of the column stack view
[columnView addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:columnView attribute:NSLayoutAttributeWidth multiplier:1 constant:0]];
[columnView addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:columnView attribute:NSLayoutAttributeHeight multiplier:1.0 / (CGFloat)n constant:0]];
}
}
// make it square
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:rowView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:rowView attribute:NSLayoutAttributeHeight multiplier:1 constant:0]];
// center it
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:rowView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:rowView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
// ensure it never exceeds 80% of the width or height of super view
NSLayoutConstraint *maxWidthConstraint = [NSLayoutConstraint constraintWithItem:rowView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationLessThanOrEqual toItem:self.view attribute:NSLayoutAttributeWidth multiplier:0.8 constant:0];
NSLayoutConstraint *maxHeightConstraint = [NSLayoutConstraint constraintWithItem:rowView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationLessThanOrEqual toItem:self.view attribute:NSLayoutAttributeHeight multiplier:0.8 constant:0];
[self.view addConstraints:#[maxWidthConstraint, maxHeightConstraint]];
// But have it take up 80% of either the width or height of super view
NSLayoutConstraint *preferredWidthConstraint = [NSLayoutConstraint constraintWithItem:rowView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeWidth multiplier:0.8 constant:0];
preferredWidthConstraint.priority = 900;
NSLayoutConstraint *preferredHeightConstraint = [NSLayoutConstraint constraintWithItem:rowView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeHeight multiplier:0.8 constant:0];
preferredHeightConstraint.priority = 900;
[self.view addConstraints:#[preferredWidthConstraint, preferredHeightConstraint]];
That yields:
If targeting earlier iOS versions, you can add build grid yourself, but it entails adding more constraints to make everything layout properly.
One way is to use bezierPath for drawing the custom view. If you need 3*3 grid layout, you need 9 bezier paths!!!!
YOu can achieve this kind of result by using bezier path:
YOu can even enter the rows/cols you need:
declare the properties in your view .h file:
#property (assign) int row;
#property (assign) int col;
and you can call the view from your view controller using:
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
testView *tv = [[testView alloc]initWithFrame:CGRectMake(30, 50, 100, 100)];
tv.row = 3;
tv.col = 4;
[self.view addSubview:tv];
}
Here I assumed grid cell size as 20x20
-(void)drawRect:(CGRect)frame {
int x = 0, y = 0;
for (int i = 1; i <= _row*_col; i++) {
UIBezierPath* rectanglePath = [UIBezierPath bezierPathWithRect: CGRectMake(CGRectGetMinX(frame) + x + 27, CGRectGetMinY(frame) + y + 13, 20, 20)];
[UIColor.grayColor setFill];
[rectanglePath fill];
[UIColor.blackColor setStroke];
rectanglePath.lineWidth = 1;
[rectanglePath stroke];
x = x + 20;
if (i%_row == 0) {
x = 0;
y = y + 20;
}
}
}
You can playaround with the grid colors too!!!

how to add multiple images with each of its constraints using For/While loop

What i want to achieve is set buttons/images Horizontally Centered with main view, including its width set to 75% of the screen width.
I want to fit like 7 such images/buttons vertically on the screen (line-by-line).
I am using the following code, which is working perfect:
UIImageView *l1 = [[UIImageView alloc] initWithFrame:CGRectMake(20, 20, 100, 100)];
[l1 setImage:[UIImage imageNamed:#"level-1"]];
[l1 setContentMode:UIViewContentModeScaleAspectFit];
l1.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:l1];
NSLayoutConstraint *c1 = [NSLayoutConstraint
constraintWithItem:l1
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1.0f
constant:20.f
];
[self.view addConstraint:c1];
NSLayoutConstraint *c1b = [NSLayoutConstraint
constraintWithItem:l1
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0f
constant:0.f
];
[self.view addConstraint:c1b];
NSLayoutConstraint *c1c = [NSLayoutConstraint
constraintWithItem:l1
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:0.75f
constant:0.f
];
[self.view addConstraint:c1c];
Instead of repeating the same code for different images, i want to use some iteration process and increment the image name [UIImage imageNamed:#"level-xxx"]] and bind top position to the bottom position of the last added item.
How it would be possible? Thx
iOS 9 introduces a new class called UIStackView that allows you to do this :
https://www.hackingwithswift.com/read/31/2/uistackview-by-example
There are even back-ports of this to iOS 7 on Github.
Some people might downvote me for this but sometimes , when it comes to autolayout I just say , $##& this I'll write the layout code myself. And this is one of those cases (especially when UIStackView is not available).
First create all your views and store them in an array in viewDidLoad.
Override viewDidLayoutSubviews on your view controller and loop through the array , set the frames and lay them out one by one.
-(void)viewDidLayoutSubviews
{
NSArray* imageList = self.imageViewList
// calculate it if necessary
CGPoint startPoint = CGPointMake(200,200)
CGFloat spacing = 50
for(UIView *view in imageList) {
view.center = startPoint
startPoint = CGPointMake(startPoint.x , startPoint.y + spacing)
}
}
This is way more obvious , way easier to debug than a mess of constraints created in code.

Position one UIlabel under another UILabel (top label has varying height)

I have two UILabels inside of a XIB, and I want to position one label underneath of another label. That said, the top label's height (descriptionLabel) varies. Does anyone know how I can go about doing this? I feel like I've tried everything.
Here is the code for my Labels so far; I want to position my second label (bodyLabel) about 25 pixels below descriptionLabel (regardless of how long descriptionLabel is):
CGRect frame = descriptionLabel.frame;
frame.origin.y=400;//pass the cordinate which you want
frame.origin.x= 12;//pass the cordinate which you want
descriptionLabel.frame= frame;
CGRect frame2 = bodyLabel.frame;
bodyLabel.frame= frame;
do this in viewDidLayoutSubviews;
-(void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
/* set label1's frame first */
CGRect newFrame = _label2.frame;
newFrame.origin.y = CGRectGetMaxY(_label1.frame)+25;
_label2.frame = newFrame;
}
CGRectGetMaxY takes the frame's origin into account when returning a value. keep in mind that frames are not yet set for views if you're doing things in loadView or viewDidLoad, this could be why things keep ending up with a 0 origin - they are still 0 at that time.
Suppose you have two UILabels. Say, firstLabel and secondLabel. Suppose you have set the first frame like so:
firstLabel.frame = CGRectMake(0,0,50,50);
If your first frame dynamically changes its height, and if want your secondLabel to be always under the first, you can set the y coordinate of the secondLabel in such a way it is always under it. The code for it can be something like:
secondLabel.frame = CGRectMake(0,firstLabel.frame.size.height,50,50);
Using this, the y position of the secondLabel is dynamic and is dependent on the firstLabel's height.
In your case, the position of the bodyLabel can be :
CGRect bodyLabelFrame = CGRectMake(0,descriptionLabel.frame.size.height,50,50);
bodyLabel.frame = bodyLabelFrame;
Have you tried using autolayout? Using autolayout, this is how I might do this if the superview for the labels was superView:
// This is necessary to use autolayout
descriptionLabel.translatesAutoResizingMasksIntoConstraints = NO;
bodyLabel.translatesAutoResizingMasksIntoConstraints = NO;
NSDictionary *views = NSDictionaryOfVariableBindings(descriptionLabel, bodyLabel);
// this will pin the top of bodyLabel to the bottom of the descriptionLabel with a gap of 25px
[superView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[descriptionLabel]-25-[bodyLabel]" options:nil metrics:nil views:views]];
You'll need to do it in code. I use auto-layout.
First, create two private NSLayoutContstraint variables for your two labels — you'll use these to adjust your label height when you set the text.
#interface CustomView ()
#property (strong, nonatomic) NSLayoutConstraint *firstLabelHeightCn;
#property (strong, nonatomic) NSLayoutConstraint *secondLabelHeightCn;
#end
Second, define the first labels X, Y, and width — the height will be set depending on the text you set in it.
NSLayoutConstraint *cnX;
NSLayoutConstraint *cnY;
NSLayoutConstraint *cnWidth;
// first label
_firstLabel = [[UILabel alloc] init];
_firstLabel.lineBreakMode = NSLineBreakByWordWrapping;
_firstLabel.numberOfLines = 0;
_firstLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_firstLabel];
cnX = [NSLayoutConstraint constraintWithItem:_firstLabel attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:H_MARGIN];
cnY = [NSLayoutConstraint constraintWithItem:_firstLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:V_MARGIN];
cnWidth = [NSLayoutConstraint constraintWithItem:_firstLabel attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1.0 constant:-H_MARGIN];
_firstLabelHeightCn = [NSLayoutConstraint constraintWithItem:_firstLabel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeHeight multiplier:0 constant:0];
[self addConstraints:#[ cnX, cnY, cnWidth, _firstLabelHeightCn ]];
Third, define the second labels X, Y, and width off the first labels properties. For the Y position you'll want to set the second labels TOP to the first labels BOTTOM (+ any margin).
// second label
_secondLabel = [[UILabel alloc] init];
_secondLabel.lineBreakMode = NSLineBreakByWordWrapping;
_secondLabel.numberOfLines = 0;
_secondLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_secondLabel];
cnX = [NSLayoutConstraint constraintWithItem:_secondLabel attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:_firstLabel attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0];
cnY = [NSLayoutConstraint constraintWithItem:_secondLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:_firstLabel attribute:NSLayoutAttributeBottom multiplier:1.0 constant:V_MARGIN];
cnWidth = [NSLayoutConstraint constraintWithItem:_secondLabel attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:_firstLabel attribute:NSLayoutAttributeRight multiplier:1.0 constant:0];
_secondLabelHeightCn = [NSLayoutConstraint constraintWithItem:_secondLabel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeHeight multiplier:0 constant:0];
[self addConstraints:#[ cnX, cnY, cnWidth, _secondLabelHeightCn ]];
Finally, create two methods to set the text for your two labels. These methods will take the incoming text, calculate the height, adjust your layout constraint constants, and then set the actual text in the label. Since you're using auto-layout once you change the text/height of the first label, the second label will automatically adjust.
- (void)setFirstText:(NSString *)firstText
{
_firstText = firstText;
if (_firstText.length) {
_firstLabelHeightCn.constant = [CustomView textHeight:_firstText width:self.bounds.size.width font:_firstLabel.font];
_firstLabel.text = _firstText;
} else {
_firstLabelHeightCn.constant = 0;
_firstLabel.text = nil;
}
}
- (void)setSecondText:(NSString *)secondText
{
_secondText = secondText;
if (_secondText.length) {
_secondLabelHeightCn.constant = [CustomView textHeight:_secondText width:self.bounds.size.width font:_secondLabel.font];
_secondLabel.text = _secondText;
} else {
_secondLabelHeightCn.constant = 0;
_secondLabel.text = nil;
}
}
Here is a real-life example:
ContextView.h
https://gist.githubusercontent.com/rosem/4fc7f9ed80c114ba45a0/raw/05f46c0340e1682823d6bbeb95f8b084ba4449d5/gistfile1.mm
ContextView.m
https://gist.githubusercontent.com/rosem/6d768776991569496ab6/raw/76ce4f47b3f86555ee4755e7d52d12511adcec27/gistfile1.m

iOS - Pure AutoLayout and UIScrollView not scrolling

This is my first time using UIScrollViews with a pure Autolayout approach. This is what the view hierarchy looks like
view
-scrollview
--view1
--view2
--view3
scrollview should contain view1|view2|view3 in that order.
I set the scrollviews width, height, centerx and bottom space to superview. The view1, view2 and view3 that are created all have their width and height constraints setup in their updateConstraints method. Additionally, some constraints are provided in code. What is the reason this scrollview is not scrolling from left to right? I have read literally all of the guides I can find online about creating and adding subviews to a UIScrollView programmatically with auto layout. I found some mention about having to provide four different constraints, leading, trailing, top and bottom for each view added as a subview to the scrollview. Are these the only NSLayoutAttributes that one can specify? How do attributes such as NSLayoutAttribueLeft or NSLayoutAttribueRight relate? I have read documentation on Apples website as well, specifically https://developer.apple.com/library/ios/technotes/tn2154/_index.html. I am attaching the setup I currently have. Everything is done via code.
- (void)viewDidLoad
{
[super viewDidLoad];
self.dataSource = #[ [[PCCGenericRating alloc] initWithTitle:#"Easiness"
andMessage:#"WHAT A JOKERRRR"
andVariatons:#[ #"very easy", #"easy", #"moderate", #"hard", #"very hard"]],
[[PCCGenericRating alloc] initWithTitle:#"Joker"
andMessage:#"WHAT A JOKERRRR"
andVariatons:#[ #"very easy", #"easy", #"moderate", #"hard", #"very hard"]],
[[PCCGenericRating alloc] initWithTitle:#"Difficulty"
andMessage:#"YOu are not difficult at all"
andVariatons:#[ #"very easy", #"easy", #"moderate", #"hard", #"very hard"]]
];
[self initView];
}
- (void)initView {
CGFloat navigationBarHeight = self.navigationController.navigationBar.frame.size.height;
CGFloat statusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.height;
CGFloat heightDifference = navigationBarHeight + statusBarHeight;
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.delegate = self;
[self.scrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
self.scrollView.backgroundColor = [UIColor greenColor];
[self.view addSubview:self.scrollView];
//setup constraints
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollView attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:1.0f
constant:0.0f]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollView attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeHeight
multiplier:1.0f
constant:-heightDifference]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollView attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0f
constant:0.0f]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollView attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeBottom
multiplier:1.0f
constant:0.0]];
[self.dataSource enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
PCCGenericRating *rating = (PCCGenericRating *)obj;
PCCGenericRatingView *ratingView = [self createViewWithRating:rating];
[self.scrollView addSubview:ratingView];
int multiplier = (idx == 0) ? 1 : (int) (idx + 1) ;
[self.scrollView addConstraint:[NSLayoutConstraint constraintWithItem:ratingView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.scrollView
attribute:NSLayoutAttributeCenterX
multiplier:multiplier
constant:0.0f]];
[self.scrollView addConstraint:[NSLayoutConstraint constraintWithItem:ratingView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.scrollView
attribute:NSLayoutAttributeCenterY
multiplier:1.0f
constant:0.0f]];
}];
}
- (PCCGenericRatingView *)createViewWithRating:(PCCGenericRating *)rating {
PCCGenericRatingView *view = [PCCGenericRatingView genericRatingViewWithTitle:rating.title andMessage:rating.message];
return view;
}
Upon printing out the scrollview constraints, they look okay to me:
po self.scrollView.constraints
<__NSArrayM 0x115b051f0>(
<NSLayoutConstraint:0x1145d9290 PCCGenericRatingView:0x114579880.centerX == UIScrollView:0x11458d4b0.centerX>,
<NSLayoutConstraint:0x1145d9410 PCCGenericRatingView:0x114579880.centerY == UIScrollView:0x11458d4b0.centerY>,
<NSLayoutConstraint:0x1145d9dd0 PCCGenericRatingView:0x1145d9560.centerX == 2*UIScrollView:0x11458d4b0.centerX>,
<NSLayoutConstraint:0x1145d9e40 PCCGenericRatingView:0x1145d9560.centerY == UIScrollView:0x11458d4b0.centerY>,
<NSLayoutConstraint:0x1145da6b0 PCCGenericRatingView:0x1145d9e90.centerX == 3*UIScrollView:0x11458d4b0.centerX>,
<NSLayoutConstraint:0x1145da730 PCCGenericRatingView:0x1145d9e90.centerY == UIScrollView:0x11458d4b0.centerY>
)
Here is a screenshot of what it looks like:
I find it odd that the last element in the datasource is the first view controller showing up in the scrollview, when it should be the last view. It also doesn't scroll left to right as it should.
Make sure your top_constraint for the view1 and bottom_constraint for view3 will be as per your scrollView's constraints. Otherwise scrollview's contentSize: {0, 0}.
Wherever you are printing your constraints, try printing scrollview.contentSize, it will likely be 0,0 and that is where your problem is. As far as I know, and as you mentioned in your post, you have to explicitly set the subviews of a scrollview to the scrollviews top bottom left and right constraints. Setting these automatically sets the contentSize of the scrollview which will enable it to scroll. It looks like you are only setting centerX and centerY constraints which will not set the scrollviews contentSize to what you need.
Try setting these programatically (this is pseudocode but you get the idea):
view1.topConstraint = scrollView.topConstraint
view1.leftConstraint = scrollView.leftConstraint
view3.bottomConstraint = scrollView.bottomConstraint
view3.rightConstraint = scrollView.rightConstraint
If you set all of those correctly, your scrollview will scroll properly. Just remember to check the contentsize, and if the contentsize is 0,0 then your constraints aren't properly set up.

Resources