Aligning a view next to another view using FLKAutoLayout - ios

I would like to align two views (in this case UIButton instances) next to each other. I want the first button to be left aligned to its superview, which is easy, but I don't see a way to make the second button aligned next to the first one without referencing the first ones width.
Here's what I'm trying right now:
UIView *superView = ...;
UIButton *buttonOne = [UIButton buttonWithType:UIButtonTypeCustom];
buttonOne.translatesAutoresizingMaskIntoConstraints = NO;
[superView addView:buttonOne];
[buttonOne constrainWidth:#"123" height:HEADER_HEIGHT_STRING];
[buttonOne alignTop:nil leading:nil superView];
UIButton *buttonTwo = [UIButton buttonWithType:UIButtonTypeCustom];
buttonTwo.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:buttonTwo];
[buttonTwo constrainWidth:#"345" height:HEADER_HEIGHT_STRING];
[buttonTwo alignLeadingEdgeWithView:buttonOne predicate:#"123"]
How do I avoid the #"123" in the last line of the code? I want it to just use the width of buttonOne.

The answer in my case was not to use FLKAutoLayout and to learn to us. NSLayoutConstraint directly like so:
NSDictionary *views = NSDictionaryOfVariableBindings(buttonOne, buttonTwo);
[superView addConstraint:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[buttonOne][buttonTwo]-|" options:0 metrics:nil views:views]];

Related

Layout constraints for dynamically created buttons

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!

Resizing UITextView Without Losing Constraints

I'm trying to figure out how to resize UITextView (and everything actually) without losing constraints. Basically, I'm trying to layout a page where most components can have variable sizes (like description). I tried doing it with a simple use case where I have a UITextView and a UIButton underneath. I want to make sure that the position of the button is relative to the bottom of the UITextView.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
CGRect frame = self.textView.frame;
int height = self.textView.contentSize.height;
frame.size.height = height;
self.textView.frame = frame;
}
What I ended up with is UITextView overlapping with UIButton. After doing a bit of research, it seems that if I replace the frame, all constraints are gone also. I tried copying the constraints over, but of course the pointer is still pointing at the old frame so that didn't help at all.
Is there a good way to solve a very dynamically laid out page? I'm trying to at least use interface builder rather than code everything.
EDITED
I tried updating the constraint as suggested, but that didn't actually resize the UITextView. Did I do it incorrectly? When I get the constant again, it's updated, but the height isn't changed visually. I did simplify my code by adding an IBOutlet for the constraint. Still no luck however.
int height = self.textView.contentSize.height;
self.textViewHeightConstraint.constant = height;
EDITED 2
I figured it out now. I had an extra constraint for the bottom and that was stopping me from actually resizing the UITextView.
The issue is how you've defined your button's top constraint. If it's to the label, when you adjust the label's height constraint, the button will move. For example, if doing it programmatically:
UILabel *label = [[UILabel alloc] init];
label.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:label];
label.text = #"Hello world";
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[button setTitle:#"Submit" forState:UIControlStateNormal];
button.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:button];
NSDictionary *views = NSDictionaryOfVariableBindings(label, button);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[label]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[button]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[label]-[button]" options:0 metrics:nil views:views]];
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:20];
[label addConstraint:heightConstraint];
Then, if you change the label's height constraint, the button will move:
heightConstraint.constant = 100;
[UIView animateWithDuration:1.0 animations:^{
[self.view layoutIfNeeded];
}];
If you've defined your UI in Interface Builder, select the button and check the top constraint of the button and make sure it's to the label, not the superview:
But, again, if the button's top constraint is to the label, when the label's height constraint changes, the button will move.

Best practice for sizing UIButtons with autolayout in code?

I'm using autolayout in code not IB to add a custom button to a UIToolbar. My question is about best practice. Do I use the following code to add, set and size the button within the toolbar:
(1)
//-------------------------------------------------------
// Toobar View
//-------------------------------------------------------
UIToolbar *toolbar = [UIToolbar new];
UIButton *addButton = [UIButton buttonWithType:UIButtonTypeCustom];
[addButton setImage:[UIImage imageNamed:#"add_normal"] forState:UIControlStateNormal];
[addButton setImage:[UIImage imageNamed:#"add_highlighted"] forState:UIControlStateHighlighted];
[addButton addTarget:self action:#selector(addItem:) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem *addNewItemButton = [[UIBarButtonItem new] initWithCustomView:addButton];
[toolbar setItems:[NSArray arrayWithObject:addNewItemButton] animated:NO];
// Add Views to superview
[superview addSubview:topbarView];
[superview addSubview:_tableView];
[superview addSubview:toolbar];
[toolbar addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"H:|-(10)-[addButton(39)]"
options:0
metrics:nil
views:viewsDictionary]];
[toolbar addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"V:|-(7)-[addButton(29)]"
options:0
metrics:nil
views:viewsDictionary]];
(2) Or do I size the button using different code like this to set the frame size:
CGRect buttonFrame = addButton.frame;
buttonFrame.size = CGSizeMake(19, 19);
addButton.frame = buttonFrame;
So is Number 1 the recommended way? I have read that settings frames is a plain no in autolayout world?
Setting the frame of a view is fine, if the view has translatesAutoresizingMaskToConstraints set to YES. Lots of system views use this setting. This setting is the default for views you create in code. Views loaded from a nib (or storyboard) have it set to NO if the nib is set to use auto layout.
Don't try to set constraints between your button and the toolbar. The toolbar doesn't use constraints to lay out its item views. (You can see this by poking around the view hierarchy in the debugger.)
Just set addButton.frame.size or addButton.bounds.size to the size you want the button to have, and set addNewItemButton.width to zero. Setting the item width to zero tells the toolbar to use the button's own size. The toolbar will center the button vertically. If you want to add horizontal spacing before the button, insert another UIBarButtonItem with a type of UIBarButtonSystemItemFixedSpace.

UIScrollView with iOS Auto Layout Constraints: Wrong size for subviews

I'm trying to generate a view in code. Here's the hierachy of my view object
UIScrollView
UIView
UIButton
The ScrollView should be the same size as the window.
The button should be as big as possible.
I'm using iOS auto layout, so the constraint strings for all of my objects look like this
H:|[object]|
V:|[object]|
I've also set translatesAutoresizingMaskIntoConstraints to NO for each object.
The problem is that the button only gets the default button-size. Its parent view object (UIView) only gets the size its subviews need.
red: UIScrollView / yellow: UIView
How can I force those views to be as big as the scrollView?
When I use a UIView instead of th UIScrollView everything works great...
Here's some code:
- (void) viewDidLoad {
[super viewDidLoad];
// SCROLL VIEW
UIScrollView* scrollView = [UIScrollView new];
scrollView.backgroundColor=[UIColor redColor];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
//CONTAINER VIEW
UIView *containerView = [UIView new];
containerView.translatesAutoresizingMaskIntoConstraints = NO;
containerView.backgroundColor = [UIColor yellowColor];
[scrollView addSubview:containerView];
// CONSTRAINTS SCROLL VIEW - CONTAINER VIEW
[scrollView addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[containerView]|"
options:0 metrics:nil
views:#{#"containerView":containerView}]];
[scrollView addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[containerView]|"
options:0 metrics:nil
views:#{#"containerView":containerView}]];
// BUTTON
UIButton* button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.translatesAutoresizingMaskIntoConstraints = NO;
[button setTitle:#"I'm way to small" forState:UIControlStateNormal];
[containerView addSubview:button];
// CONSTRAINTS CONTAINER VIEW - BUTTON
[containerView addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[button]|"
options:0 metrics:nil
views:#{#"button":button}]];
[containerView addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[button]|"
options:0 metrics:nil
views:#{#"button":button}]];
self.view = scrollView;
}
UPDATE:
I really don't know, why this is happening. If you set up the view in IB, connect the outlets and instanciate the view in code, the scrollview behaves like a normal view (which bounces vertically). Its contentSize is not calculated correctly. More here. But how to do it correctly?
A couple of observations:
Constraints for subviews in scroll views don't work like constraints in other views. They're used to set the contentSize of the scroll view. (See TN2154.) That way, you throw a bunch of stuff on a scroll view, set the constraints for the stuff inside it, and the contentSize is calculated for you. It's very cool feature, but it's antithetical to what you're trying to do here.
Worse, buttons will, unless you set an explicit constraint for their width and height of a button, will resize according to their content.
The net effect of these two observations is that your existing constraints say "(a) set my container to be the size of my button; (b) let my button resize itself dynamically to the size of the text; and (c) set my scrollview's contentSize according to the size of my container (which is the size of the button)."
I'm unclear as to what the business problem is. But here are some constraints that achieve what I think your technical question was:
- (void)viewDidLoad
{
[super viewDidLoad];
UIView *view = self.view;
UIScrollView *scrollView = [[UIScrollView alloc] init];
scrollView.backgroundColor = [UIColor redColor]; // just so I can see it
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:scrollView];
UIView *containerView = [[UIView alloc] init];
containerView.backgroundColor = [UIColor yellowColor]; // just so I can see it
containerView.translatesAutoresizingMaskIntoConstraints = NO;
[scrollView addSubview:containerView];
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.translatesAutoresizingMaskIntoConstraints = NO;
[button setTitle:#"I'm the right size" forState:UIControlStateNormal];
[containerView addSubview:button];
NSDictionary *views = NSDictionaryOfVariableBindings(scrollView, button, view, containerView);
// set the scrollview to be the size of the root view
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[scrollView]|"
options:0
metrics:nil
views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[scrollView]|"
options:0
metrics:nil
views:views]];
// set the container to the size of the main view, and simultaneously
// set the scrollview's contentSize to match the size of the container
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[containerView(==view)]|"
options:0
metrics:nil
views:views]];
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[containerView(==view)]|"
options:0
metrics:nil
views:views]];
// set the button size to be the size of the container view
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[button(==containerView)]"
options:0
metrics:nil
views:views]];
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[button(==containerView)]"
options:0
metrics:nil
views:views]];
}
Frankly, I don't understand the business intent of your UI, as this feels like a contortion of auto layout to achieve a very simply UI. I don't know why you have a scroll view if you have "screen sized" content in it (unless you were paging through buttons). I don't know why you'd have a content view with a single item in it. I don't understand why you're using a full-screen button (I'd just put a tap gesture on the root view at that point and call it a day).
I'll assume you have good reasons for all of this, but it might make sense to back up, ask what is your desired user experience is, and then approach the problem fresh to see if there's a more efficient way to achieve the desired effect.

ios UIViewController programmatically positioning buttons not working

I would like to evenly space four buttons across a view. In the storyboard I have positioned the buttons in a portrait view so the spacing is correct. But I did not find the correct constraint settings to make the buttons space themselves evenly for any view width (for portrait iPad or landscape orientations). So, I added the following code snippet that moves the buttons to desired locations using the 1st and 4th buttons as the anchors:
// evenly space the buttons
CGPoint leftPoint = self.button1.center;
CGPoint rightPoint = self.button4.center;
CGFloat width = rightPoint.x - leftPoint.x;
leftPoint.x += width / 3;
rightPoint.x -= width / 3;
self.button2.center = leftPoint;
self.button3.center = rightPoint;
The positioning code is working fine, but my difficulty is finding the best place to make the adjustments. - (void)viewDidAppear:(BOOL)animated
seems to be the best spot. However, if I seque to a different view, when I return to this view the buttons will have reverted to their initial (storybaord constraint) specified positions. The viewDidAppear code will get called again but it does not succeed at moving the buttons. It is as if their positions are locked at that point in time.
I guess my primary question is if there is a way to use constraints to achieve the even spacing I am after. Or secondary question is how to override the auto positioning of those two buttons.
This is a relatively hard thing to do using layout constraints, and it depends on exactly what you want. I have an example here that creates 4 buttons (in code) along with 5 labels that are used as spacers between the buttons. The buttons' sizes are determined by their intrinsic content size, and the spacing among the buttons and between the buttons and the sides of the containing view are all the same.
-(void)viewDidLoad {
[super viewDidLoad];
NSMutableDictionary *viewsDict = [NSMutableDictionary dictionary];
NSArray *titles = #[#"Short",#"Longer",#"Short",#"The Longest"];
for (int i=1; i<5; i++) {
UIButton *b = [UIButton buttonWithType:1];
[b setTitle:titles[i-1] forState:UIControlStateNormal];
[b setTranslatesAutoresizingMaskIntoConstraints:NO];
[viewsDict setObject:b forKey:[NSString stringWithFormat:#"b%d",i]];
}
for (int i=1; i<6; i++) {
UILabel *l = [[UILabel alloc ]init];
[l setTranslatesAutoresizingMaskIntoConstraints:NO];
[viewsDict setObject:l forKey:[NSString stringWithFormat:#"l%d",i]];
}
for (id obj in viewsDict.allKeys)
[self.view addSubview:viewsDict[obj]];
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|[l1][b1][l2(==l1)][b2][l3(==l1)][b3][l4(==l1)][b4][l5(==l1)]|"
options:NSLayoutFormatAlignAllBaseline
metrics:nil
views:viewsDict];
NSArray *constraints2 = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[b1]-|"
options:0
metrics:nil
views:viewsDict];
[self.view addConstraints:constraints];
[self.view addConstraints:constraints2];
}
The spacing of the buttons will automatically adjust when the view size changes, as on a rotation.
The solution I was led to is to programmatically add constraints to the two middle buttons (button2 & button3) that position them horizontally relative to the middle of the view. These two constraints allowed me to completely remove the manual positioning code. The answer to Evenly space multiple views within a container view helped get me on the right track.
- (void)viewDidLoad
{
...
self.button2.translatesAutoresizingMaskIntoConstraints = NO;
self.button3.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.button2 attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual
toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:0.667 constant:0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.button3 attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual
toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.333 constant:0]];
For almost any case of layout problem use layoutSubviews method, it's the place to do it.

Resources