I am trying to create a view with dynamically created buttons. I am finding it difficult to set the constraints for inner objects other than first one that's created. Where is the issue?
Create & Add buttons to view
-(void) createButton:(NSString *) btnText isButton:(BOOL) type phraseWidth:(NSInteger) width view:(UIView *) currentView {
if (!type) { // if it's a button then create label & button at same place else only create button
// align left to prev button, align baseline
if (prevX == 5) { // button left aligned to rowView, right align none
UIButton *btnView = [[UIButton alloc] init];
btnView.translatesAutoresizingMaskIntoConstraints=NO;
[currentView addSubview:btnView];
NSDictionary *dictScrollConst = NSDictionaryOfVariableBindings(btnView);
NSString *hConstraint = [NSString stringWithFormat:#"H:|-%f-[btnView]|",prevX];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:hConstraint options:0 metrics:nil views:dictScrollConst]];
NSString *vConstraint = #"V:|[btnView]|";
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:vConstraint options:0 metrics:nil views:dictScrollConst]];
prevObject = btnView;
}
else { // align new button to previous button
UIButton *btnView = [[UIButton alloc] init];
btnView.translatesAutoresizingMaskIntoConstraints=NO;
[currentView addSubview:btnView];
NSDictionary *dictScrollConst = NSDictionaryOfVariableBindings(prevObject,btnView);
NSString *hConstraint = [NSString stringWithFormat:#"H:[prevObject]-%d-[btnView]",kHorizontalSidePadding];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:hConstraint options:0 metrics:nil views:dictScrollConst]];
NSString *vConstraint = #"V:|[btnView]|";
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:vConstraint options:0 metrics:nil views:dictScrollConst]];
}
}
}
Doesn't allow constraint to be added with respect to previous button created. Throws up exception:
Impossible to set up layout with view hierarchy unprepared for constraint
There's still way too much code for me to work out what's going on, but this much is obvious:
NSString *vConstraint = #"V:|[btnView]|";
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:vConstraint options:0 metrics:nil views:dictScrollConst]];
[currentView addSubview:btnView];
Those lines are in the wrong order. You cannot add a constraint involving a view (here, btnView) at a time when that view is not in the view hierarchy. (That is exactly what the error message is telling you, though granted it phrases it in rather coy terminology.)
So, add the subview. Then add the constraint that affects it.
What I suggest you do is what I always do: start very simple and work your way up to the full extent of the actual problem. So, I suggest as an exercise that you begin with the second row of your layout, and see if you can do just this simple exercise: given the array of titles #[#"Yellow", #"Purple", #"Blue", #"Red"], can you use it to generate four buttons horizontally?
Here's my code for doing that. Notice how clear and simple it is - ruthlessly logical, spare, and plain. We can always add tweaks later, but this is the kind of simplicity you need to try to maintain and build upon, so that you don't confuse yourself:
NSArray* titles = #[#"Yellow", #"Purple", #"Blue", #"Red"];
UIView* previousButton = nil;
for (NSInteger i = 0; i < 4; i++) {
UIButton* b = [UIButton buttonWithType:UIButtonTypeSystem];
[b setTitle:titles[i] forState:UIControlStateNormal];
[b setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:b];
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-(100)-[b]"
options:0 metrics:nil views:#{#"b":b}]];
if (i == 0) {
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(50)-[b]"
options:0 metrics:nil views:#{#"b":b}]];
} else {
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:[p]-(20)-[b]"
options:0 metrics:nil views:#{#"b":b, #"p":previousButton}]];
}
previousButton = b;
}
Given this, we see at once one of the things wrong with your code: there is no evidence that you are setting the previous button (your prevObject) on any except the first pass, when of course you need to do it on every pass.
Once we have code that works, we can start to modify it to approach what you are wishing to do. For example, it is now easy to change the hard-coded spacing to use variables like yours instead:
NSArray* titles = #[#"Yellow", #"Purple", #"Blue", #"Red"];
UIView* previousButton = nil;
NSInteger initialX = 5; // *
NSInteger horizSpace = 10; // *
for (NSInteger i = 0; i < 4; i++) {
UIButton* b = [UIButton buttonWithType:UIButtonTypeSystem];
[b setTitle:titles[i] forState:UIControlStateNormal];
[b setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:b];
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-(100)-[b]"
options:0 metrics:nil views:#{#"b":b}]];
if (i == 0) {
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(initialX)-[b]"
options:0 metrics:#{#"initialX":#(initialX)} views:#{#"b":b}]];
} else {
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:[p]-(horizSpace)-[b]"
options:0 metrics:#{#"horizSpace":#(horizSpace)} views:#{#"b":b, #"p":previousButton}]];
}
previousButton = b;
And so forth. The point is: This is how I "grow my code", starting always with the simple and evolving, making sure it works at every iteration, until I reach the thing I'm really trying to do. Go ye and do likewise!
Related
Here is the code I have...this is kind of a simple question...perhaps I shouldn't do it with UIView...but I just wanna draw a decorative horizontal line to separate content. What I am getting is no error the line just does not draw. If I init it with a frame and turn off autolayout, then I do see the line but that's not how I want to implement it since everything is autolayout.
Below the viewDidLoad calls the other methods to create the UI elements and add the constraints.
- (void)viewDidLoad {
[super viewDidLoad];
// Add main layout
[self.scrollView addSubview:self.profileInfoContainer];
// Add UI elements
[self.profileInfoContainer addSubview:self.email];
[self.profileInfoContainer addSubview:self.userName];
[self.profileInfoContainer addSubview:self.hLineSeperator];
[self.profileInfoContainer addSubview:self.navigationContainer];
// Add Constraints
[self addInfoContainerConst];
}
Below i add the constraints for all ui elements in the profileInfoContainer, including the hLineSeperator
- (void)addInfoContainerConst {
// Add constrains
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_userName, _email, _hLineSeperator, _navigationContainer);
NSDictionary *metrics = #{
#"vTop":#60,
#"vBttm":#5,
#"hLeft":#25,
#"hRight":#25,
#"vMiddle":#5
};
NSArray *constraint_POS_V = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-vTop-[_userName]-vMiddle-[_email]-vMiddle-[_hLineSeperator]-vMiddle-[_navigationContainer]|"
options:0
metrics:metrics
views:viewsDictionary];
NSArray *constraint_POS_H = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-hLeft-[_userName]-hRight-|"
options:0
metrics:metrics
views:viewsDictionary];
NSArray *constraint_POS_H_1 = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-hLeft-[_email]-hRight-|"
options:0
metrics:metrics
views:viewsDictionary];
NSArray *constraint_POS_H_2 = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_hLineSeperator(1)]|"
options:0
metrics:metrics
views:viewsDictionary];
NSArray *constraint_POS_H_3 = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_navigationContainer]|"
options:0
metrics:metrics
views:viewsDictionary];
[self.profileInfoContainer addConstraints:constraint_POS_V];
[self.profileInfoContainer addConstraints:constraint_POS_H];
[self.profileInfoContainer addConstraints:constraint_POS_H_1];
[self.profileInfoContainer addConstraints:constraint_POS_H_2];
[self.profileInfoContainer addConstraints:constraint_POS_H_3];
}
Below is the method to create the UIView that is used to draw the line
- (UIView*)hLineSeperator {
if (!_hLineSeperator) {
_hLineSeperator = [UIView new];
_hLineSeperator.translatesAutoresizingMaskIntoConstraints = NO;
_hLineSeperator.backgroundColor = [UIColor grayColor];
}
return _hLineSeperator;
}
It is not required to add constraints to subviews (specifically like the ones you want to create) created in code. You can do it something like,
UIView *horizontalSeperatorViewOnTop = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.(yourView).frame.size.width, 0.5f)];
horizontalSeperatorViewTop.backgroundColor = [UIColor lightGrayColor];
[self.(yourView) addSubview: horizontalSeperatorViewOnTop];
I do this in my code all the time for horizontal or vertical separators and it works great!
I'm learning auto layout and I'd like to setup a set of buttons to be vertically stacked and evenly spaced. I'd also like the buttons pinned to the bottom of the view. What's a good way to setup these constraints with VFL? The button list will be passed in as an array of UIButtons.
NSArray *buttons = [button1, button2, button3, button4, ...]
NSMutableArray *allConstraints = [NSMutableArray array]
UIButton *previousButton;
for (UIButton button in buttons) {
// Buttons take up full width
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"H:|[button]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(button);];
[allConstraints addObjectsFromArray:constraints];
constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"V:|[button]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(button);];
[allConstraints addObjectsFromArray:constraints];
if (!previousButton) {
NSDictionary *metrics = #{#"padding" : #(10)};
// Make buttons height
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[-(padding)-previousButton(==button)]"
options:0
metrics:metrics
views:NSDictionaryOfVariableBindings(previousButton, button)];
[allConstraints addObjectsFromArray:constraints];
}
previousButton = button;
}
[self.view addConstraints:allConstraints]
This doesn't achieve what I need as the buttons don't get pinned to the bottom of the view.
I would do it in a somewhat different way. Rather than building up the constraints inside a loop, I would build the format string in a loop.
-(void)addButtonsWithConstraints:(NSArray *) buttons {
NSMutableDictionary *views = [NSMutableDictionary new];
for (int i = 0; i<buttons.count; i++) {
[buttons[i] setTranslatesAutoresizingMaskIntoConstraints: NO];
[self.view addSubview:buttons[i]];
[views setObject:buttons[i] forKey:[NSString stringWithFormat:#"button%d",i]];
}
NSMutableString *formatString = [#"V:" mutableCopy];
for (int i = 0; i<buttons.count-1; i++) {
[formatString appendFormat:#"[button%d]-10-", i];
}
[formatString appendFormat:#"[button%lu]|", buttons.count - 1]; // pins the last button to the bottom of the view
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:formatString options:NSLayoutFormatAlignAllLeft | NSLayoutFormatAlignAllRight metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[button0]|" options:0 metrics:nil views:views]];
}
The last line sets button0 to the full width of the view, and the format options in the previous line make all the buttons align their left and right edges.
Hi this might help you out
NSArray *buttons = #[button1, button2, button3, button4, button5];
NSMutableArray *allConstraints = [NSMutableArray array];
UIButton *previousButton;
for (UIButton *button in buttons) {
[button setTranslatesAutoresizingMaskIntoConstraints:NO];
// Buttons take up full width
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"H:|[button]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(button)];
[allConstraints addObjectsFromArray:constraints];
if (!previousButton) {
constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"V:|->=10-[button(65)]"
options: 0
metrics:nil
views:NSDictionaryOfVariableBindings(button)];
[allConstraints addObjectsFromArray:constraints];
}
if (previousButton) {
NSDictionary *metrics = #{#"padding" : #(10)};
// Make buttons height
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[previousButton]-(20)-[button(65)]"
options:0
metrics:metrics
views:NSDictionaryOfVariableBindings(previousButton, button)];
[allConstraints addObjectsFromArray:constraints];
}
previousButton = button;
}
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"V:[previousButton]-10-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(previousButton)];
[allConstraints addObjectsFromArray:constraints];
[self.view addConstraints:allConstraints]
I had two buttons in a UIView, then I add the view to main view, but I eventually got something like this:
As you can see these two buttons went outside the red view.
I wanted a little bit of margin on top, so I use constraintsWithVisualFormat:#"V:|-100-[buttonGroup]-10-|", I'm not sure if that matters.
Here's the original code:
- (UIButton*) getButtonWithTitle: (NSString*) title
{
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.layer.borderColor = [UIColor blackColor].CGColor;
button.layer.borderWidth = 0.5f;
button.layer.cornerRadius = 2.0f;
[button setTranslatesAutoresizingMaskIntoConstraints:NO];
[button setTitle:title forState:UIControlStateNormal];
[button setTitleColor:[UIColor blackColor] forState: UIControlStateNormal];
return button;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.view setTranslatesAutoresizingMaskIntoConstraints:NO];
UIButton *loginBtn = [self getButtonWithTitle:#"Login"];
UIButton *registerBtn = [self getButtonWithTitle:#"Register"];
UIView *buttonGroup = [[UIView alloc] init];
[buttonGroup setTranslatesAutoresizingMaskIntoConstraints:NO];
[buttonGroup addSubview: loginBtn];
[buttonGroup addSubview: registerBtn];
[self.view addSubview: buttonGroup];
[buttonGroup setBackgroundColor:[UIColor redColor]];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(loginBtn, registerBtn, buttonGroup);
[buttonGroup addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[loginBtn]-20-[registerBtn(==loginBtn)]-|"
options:0
metrics:nil
views:viewsDictionary
]];
NSArray *verticalConstraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"V:|-100-[buttonGroup]-10-|"
options:0
metrics:nil
views:viewsDictionary];
NSArray *horizontalConstraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"H:|-[buttonGroup]-|"
options:0
metrics:nil
views:viewsDictionary];
[self.view addConstraints: horizontalConstraints];
[self.view addConstraints: verticalConstraints];
}
EDIT
The constraints I added:
[buttonGroup addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[loginBtn]-|"
options:0
metrics:nil
views:viewsDictionary
]];
[buttonGroup addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[registerBtn]-|"
options:0
metrics:nil
views:viewsDictionary
]];
It looks like I did extra work ...
You never created any vertical constraints between the buttons and buttonGroup; add those, and you should be good. Also, you shouldn't set translatesAutoresizingMaskIntoConstraints to NO for the controller's self.view (only for subviews that you add to it).
In the VFL , the | means the superview, but your two buttons(loginBtn, registerBtn) are the subviews of the buttonGroup. So you should define the V relation about the loginBtn, registerBtn with the buttonGroup. As rdelmar said, the self.view should set the translatesAutoresizingMaskIntoConstraints to No. This can close the autosizingmask function. I think the best way to do this things in xib or storyboard.
I find it convenient to collect all constraints for a whole view hierarchy and adding them all to the local root view. Is this discouraged and/or a bad idea versus adding constraints to the appropriate views?
Below is a simplification of a common pattern I use. The views are often more complicated with multiple stored constraints that needs updating as views are added or removed.
- (void)updateViewConstraints
{
UIView *content = ...;
UIView *panel = ...;
UILabel *label = ...;
[self.view addSubview:content];
[content addSubview:panel];
[panel addSubview:label];
NSMutableArray *constraints = [NSMutableArray new];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"|[content]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(content)]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"|[panel]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(panel)]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"|[label]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(label)]];
[self.view addConstraints:constraints]
self.storedConstraints = constraints;
[super updateViewConstraints];
}
- (void)invalidateConstraints
{
[self.view removeConstraints:self.storedConstraints];
[self.view needsUpdateConstraints];
}
I'm trying to lay a button over a map and constrain it to 10 points in from the left and 10 points up from the bottom. However, when I run it in simulator it tells me my constraints are broken. Here's my code. What am I missing?
_map = [[MKMapView alloc] init];
[self setView:_map];
CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(39.810166, -86.156708);
_loc = [[TCMLocationPoint alloc] initWithCoordinate:coord title:#"My Location" subtitle:#"My Subtitle"];
[_loc setParent:_map];
[_map addAnnotation:_loc];
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(coord, 2000, 2000);
[_map setRegion:region animated:NO];
_directionsButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[_directionsButton addTarget:self action:#selector(getLocation:) forControlEvents:UIControlEventTouchDown];
[_directionsButton setTitle:#"Get Directions" forState:UIControlStateNormal];
[_directionsButton setFrame:CGRectMake(0.0, 0.0, 150.0, 25.0)];
[[self view] addSubview:_directionsButton];
NSDictionary *nameMap = #{#"button" : _directionsButton};
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-10-[button]-(>=0)-|"
options:0
metrics:nil
views:nameMap];
[[self view] addConstraints:horizontalConstraints];
NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-(>=0)-[button]-10-|"
options:0
metrics:nil
views:nameMap];
[[self view] addConstraints:verticalConstraints];
A couple of thoughts:
I'd set translatesAutoresizingMaskIntoConstraints to NO.
You can probably simplify your VFL, too, and eliminate the >=0 stuff. You can also set the width and height right in your VFL.
You also don't need to set the frame. Let the constraints set the width, height.
Unless you're doing this in loadView, I'd suggest you add the map as a subview rather than trying to set self.view.
Thus:
_map = [[MKMapView alloc] init];
_map.delegate = self; // if you've implemented any `MKMapViewDelegate` methods, you should set your delegate
[_map setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:_map];
NSDictionary *views = #{#"map" : _map};
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[map]|"
options:0
metrics:nil
views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[map]|"
options:0
metrics:nil
views:views]];
// do your stuff where you're adding your annotation here
_directionsButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[_directionsButton addTarget:self action:#selector(getLocation:) forControlEvents:UIControlEventTouchDown];
[_directionsButton setTitle:#"Get Directions" forState:UIControlStateNormal];
[_directionsButton setTranslatesAutoresizingMaskIntoConstraints:NO];
[[self view] addSubview:_directionsButton];
NSDictionary *nameMap = #{#"button" : _directionsButton};
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-10-[button(150)]"
options:0
metrics:nil
views:nameMap];
[[self view] addConstraints:horizontalConstraints];
NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[button(25)]-10-|"
options:0
metrics:nil
views:nameMap];
[[self view] addConstraints:verticalConstraints];
You might even want to set the priority for the width and height constraints to be less than 1000, that way if the font dictates that the button should be a little larger, you'll let it grow, e.g. #"H:|-10-[button(150#500)]" and #"V:[button(25#500)]-10-|".