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

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

Related

Constraints programmatically with Objective C

I don't know what I'm doing wrong: I'm creating a UIView that occupies all the screen (it has already constraints) and then, programmatically I'm creating an UI Image View:
_panel = [[UIImageView alloc] initWithImage:[self loadImageForKey:#"registerPanel"]];
_panel.frame = CGRectMake(0, 0, 100, 100);
_panel.exclusiveTouch = YES;
_panel.userInteractionEnabled = YES,
[self.scrollView addSubview:_panel];
And here it comes the problem: I'm adding constraints to the panel I created but it crashes (I'm doing it on the ViewWillAppear):
NSLayoutConstraint *centreHorizontallyConstraint = [NSLayoutConstraint
constraintWithItem:_panel
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0];
NSLayoutConstraint *centreVerticalConstraint = [NSLayoutConstraint
constraintWithItem:_panel
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0];
[_panel addConstraint:centreHorizontallyConstraint];
[_panel addConstraint:centreVerticalConstraint];
Error message:
When added to a view, the constraint's items must be descendants of that view (or the view itself). This will crash if the constraint needs to be resolved before the view hierarchy is assembled. Break on -[UIView _viewHierarchyUnpreparedForConstraint:] to debug.
You can constrain a scrollView's subview to the scrollView's parent (self.view in this case), but that's probably not what you want.
Edit: For clarification, the reason you were getting the error was because you initialize your constraints:
toItem:self.view
and then you try to add them:
[_panel addConstraint:centreHorizontallyConstraint];
[_panel addConstraint:centreVerticalConstraint];
You want to add them to the toItem object:
[self.view addConstraint:centreHorizontallyConstraint];
[self.view addConstraint:centreVerticalConstraint];
Again, you probably don't want to center _panel in the main view, but this will compile and run:
#import "AddPanelScrollViewController.h" /// just default .h
#interface AddPanelScrollViewController ()
#property (strong, nonatomic) UIScrollView *scrollView;
#property (strong, nonatomic) UIImageView *panel;
#end
#implementation AddPanelScrollViewController
- (void)viewDidLoad {
[super viewDidLoad];
_scrollView = [UIScrollView new];
_scrollView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:_scrollView];
[_scrollView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:20.0].active = YES;
[_scrollView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-20.0].active = YES;
[_scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20.0].active = YES;
[_scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20.0].active = YES;
_scrollView.backgroundColor = [UIColor blueColor];
_panel = [UIImageView new];
// required
_panel.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:_panel];
// frame will be ignored when using auto-layout / constraints
// _panel.frame = CGRectMake(0, 0, 100, 100);
_panel.exclusiveTouch = YES;
_panel.userInteractionEnabled = YES;
_panel.backgroundColor = [UIColor redColor];
// _panel needs width and height constraints
[_panel.widthAnchor constraintEqualToConstant:100.0].active = YES;
[_panel.heightAnchor constraintEqualToConstant:100.0].active = YES;
NSLayoutConstraint *centreHorizontallyConstraint = [NSLayoutConstraint
constraintWithItem:_panel
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0];
NSLayoutConstraint *centreVerticalConstraint = [NSLayoutConstraint
constraintWithItem:_panel
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0];
// if constraints are releated to "self.view" that's where they need to be added
[self.view addConstraint:centreHorizontallyConstraint];
[self.view addConstraint:centreVerticalConstraint];
}
First you can't create constraints between panel & self.view because there is no common parent , instead you want to create them with the scrollview
NSLayoutConstraint *centreHorizontallyConstraint = [NSLayoutConstraint
constraintWithItem:_panel
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.scrollView
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0];
NSLayoutConstraint *centreVerticalConstraint = [NSLayoutConstraint
constraintWithItem:_panel
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.scrollView
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0];
[_scrollView addConstraint:centreHorizontallyConstraint];
[_scrollView addConstraint:centreVerticalConstraint];
Also both constraints are centerX , you need also width & height , or better top , leading , trailing and bottom to scrollView ,,, with width and height static or proportional to self.view
//
Also for any view you want to add constraints programmatically you must set
[self.scrollView setTranslatesAutoresizingMaskIntoConstraints: NO];
[self.panel setTranslatesAutoresizingMaskIntoConstraints: NO];

AwakeFromNib auto layout stretch UIlabel to spacing on both side programmatically

I am trying to add a UIlabel to a UIView class.
it should be in the following format -15-Label(stretch to max width)-15.
Top spacing=15 and height fixed to 30.
Two issues with the following code:-
1) Label does not stretch to max width
2) Right side spacing does not show up , if the text it too long.
-(void)awakeFromNib{
[super awakeFromNib];
view1 =[[UILabel alloc] init];
view1.translatesAutoresizingMaskIntoConstraints=NO;
[self addSubview:view1];
view1.text= #"Hello";
NSDictionary *constraintViews=
#{#"view1":view1};
NSDictionary *metrics=#{#"spacing":#(15)};
NSArray *hConstraints=[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-spacing-[view1]-spacing-|" options:NSLayoutFormatAlignAllCenterX metrics:metrics views:allViews];
NSArray *vConstraints=[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-spacing-[view1(30)]" options:0 metrics:metrics views:constraintViews];
[self addConstraints:hConstraints];
[self addConstraints:vConstraints];
}
1)
Update the horizontal constraints like so:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-spacing-[view1]-spacing#751-|" options:NSLayoutFormatAlignAllCenterX metrics:metrics views:constraintViews];
Try adding the line below:
[view1 setContentHuggingPriority:UILayoutPriorityHigh forAxis:UILayoutConstraintAxisHorizontal];
2) I always set the numberOfLines property of a label to 0 by default, so that the label will autoresize vertically if the text needs to be shown in two or more lines. That being said, you would need to remove the fixed height constraint and the label will be the size of the it's contents like so:
view1.numberOfLines = 0;
NSArray *vConstraints=[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-spacing-[view1]" options:0 metrics:metrics views:constraintViews];
I hope this helps.
I used this generic method for applying constraints of childView wrt to ParentView.Just pass your views to this method.
+ (void)applyConstraints:(UIView *)pChildView withSuperView:(UIView *)pParentView {
pChildView.translatesAutoresizingMaskIntoConstraints = NO;
// Width.
CGFloat widthValue = pParentView.frame.size.width;
[pParentView addConstraint:[NSLayoutConstraint constraintWithItem:pChildView attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual toItem:pParentView
attribute:NSLayoutAttributeWidth multiplier:1.0 constant:widthValue]];
// Height.
CGFloat heightValue = pParentView.frame.size.height;
[pParentView addConstraint:[NSLayoutConstraint constraintWithItem:pChildView attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual toItem:pParentView
attribute:NSLayoutAttributeHeight multiplier:1.0 constant:heightValue]];
// X margin.
[pParentView addConstraint:[NSLayoutConstraint constraintWithItem:pChildView attribute:NSLayoutAttributeCenterXWithinMargins
relatedBy:NSLayoutRelationEqual toItem:pParentView
attribute:NSLayoutAttributeCenterXWithinMargins multiplier:1.0 constant:0]];
// Y margin.
[pParentView addConstraint:[NSLayoutConstraint constraintWithItem:pChildView attribute:NSLayoutAttributeCenterYWithinMargins
relatedBy:NSLayoutRelationEqual toItem:pParentView
attribute:NSLayoutAttributeCenterYWithinMargins multiplier:1.0 constant:0]];
}

How to change or update NSLayoutConstraint programmatically

I have implemented AutoLayout programmatically using the Code :
- (void)addConstraintWithListNavigationViewController:(UIView *)listViewNavigation y:(CGFloat)y height:(CGFloat)height
{
//WIDTH_ListTableView = 0.4
//set x = 0;
NSLayoutConstraint *constraintToAnimate1 = [NSLayoutConstraint constraintWithItem:listViewNavigation
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeft
multiplier:0.00
constant:0];
[self.view addConstraint:constraintToAnimate1];
//set y = y;
NSLayoutConstraint *constraintToAnimate2 = [NSLayoutConstraint constraintWithItem:listViewNavigation
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeBottom
multiplier:0.00
constant:y];
[self.view addConstraint:constraintToAnimate2];
//set Width = self.view.frame.size.width*0.4
NSLayoutConstraint *constraintToAnimate3 = [NSLayoutConstraint constraintWithItem:listViewNavigation
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:1-WIDTH_ListTableView
constant:0.0];
[self.view addConstraint:constraintToAnimate3];
//Set height = height
NSLayoutConstraint *constraintToAnimate4 = [NSLayoutConstraint constraintWithItem:listViewNavigation
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:0.00
constant:height];
[self.view addConstraint:constraintToAnimate4];
}
And this works perfect, but every-time this ViewController receives a Notification, it will run:
[self.view layoutIfNeeded];
But I want to set the width of listViewNavigation according a boolean variable connected.
if(connected){
listViewNavigation.view.frame.size.width = 0.4 * self.view.frame.size.width;
}
else{
listViewNavigation.view.frame.size.width = 0.6 * self.view.frame.size.width;
}
But i do not know how can I update the NSLayoutConstraint :
NSLayoutConstraint *constraintToAnimate3 = [NSLayoutConstraint constraintWithItem:PreView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:1 - WIDTH_ListTableView
constant:0.0];
[self.view addConstraint:constraintToAnimate3];
when this ViewController receive the notification.
I think you have 2 options.
Option 1
Keep a property
#property (strong,nonatomic)NSLayoutConstraint *constraintToAnimate3;
Then use this property to
self.constraintToAnimate3 = [NSLayoutConstraint constraintWithItem:PreView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:1
constant:-1 * 0.4 * self.view.frame.size.width];
[self.view addConstraint:self.constraintToAnimate3];
When you want to change
if(connected){
self.constraintToAnimate3.constant = -1 *0.6 * self.view.frame.size.width;
}
else{
self.constraintToAnimate3.constant = -1 *0.4 * self.view.frame.size.width;
}
[UIView animateWithDuration:yourduration animations:^{
[self.view layoutIfNeeded];
}];
Option 2
Set an identifier of constraintToAnimate3
constraintToAnimate3.identifier = #"1234"
Then search to get the constraint
NSLayoutConstraint * constraint3 = nil;
NSArray * constraints = self.view.constraints;
for (NSLayoutConstraint * constraint in constraints) {
if ([constraint.identifier isEqualToString:#"1234"]) {
constraint3 = constraint;
break;
}
}
Then change the constant as shown in Option1
Update:
If use constant in the code I post
PreView.frame.size.with = self.view.size.width * multiplier + constant
OK, I figure out.
[self.view removeConstraint:self.constraintOld];
[self.view addConstraint:self.constraintNew];
[UIView animateWithDuration:time animations:^{
[self.view layoutIfNeeded];
}];
It's essential that you don't just add the constraints to your view, but that you also remember them.
It's easiest to change a constraint if you only need to change its constant - the constant is the only part of a constraint that can be changed later repeatedly. To do that, you need to store the constraint on its own.
Otherwise you need to remove old constraints and add new ones. Since you usually have more than one constraint, store arrays with sets of constraints that may need to be replaced, then update the whole array. You can also use the activateConstraints and deactivateConstraints methods.

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.

How to add constraints to items with random positions

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;
}
}
}

Resources