Best practice for sizing UIButtons with autolayout in code? - ios

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.

Related

Autoresizing not working for UIView added programatically

I have followed this SO link for autoresizing, but Autoresizing not working.
How to set frame programmatically with autoresizing?
I have set frame for iPhone 4
UIView *box = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 320, 120)];
[box setBackgroundColor:[UIColor redColor]];
[box setAutoresizingMask:UIViewAutoresizingFlexibleHeight];
[box setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
[box setAutoresizingMask:UIViewAutoresizingFlexibleTopMargin];
[box setAutoresizingMask:UIViewAutoresizingFlexibleLeftMargin];
[box setAutoresizingMask:UIViewAutoresizingFlexibleRightMargin];
[self.view addSubview:box];
But it is not working in iPad or iPhone 6
First thing first : As per open suggestion, you should use constraints.
Add autoresizing mask after adding it to view.
UIView *box = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 320, 120)];
[box setBackgroundColor:[UIColor redColor]];
[self.view addSubview:box];
[box setAutoresizingMask:UIViewAutoresizingFlexibleHeight];
[box setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
[box setAutoresizingMask:UIViewAutoresizingFlexibleTopMargin];
[box setAutoresizingMask:UIViewAutoresizingFlexibleLeftMargin];
[box setAutoresizingMask:UIViewAutoresizingFlexibleRightMargin];
If I understand you question correctly you're trying to set red view on top with height = 120
ADD EXPLANATION
You could achieve it with using constraints:
UIView *box = [[UIView alloc] init];
// Prevent creating constraints from masks automatically
box.translatesAutoresizingMaskIntoConstraints = NO;
box.backgroundColor = [UIColor redColor];
[self.view addSubview:box];
// Define metrics (constants) which will be used to create constraints.
// Key #"boxSize" - name which will be used in constraints, Value - constant
NSDictionary *metrics = #{#"boxSize" : #(120)};
// Define views that will participate in auto layout constraints.
// Key #"readBox" - name which will be used in constraints, Value - real UIView object
NSDictionary *views = #{ #"redBox" : box };
// Here we create constraints. For Vertical, and for Horizontal
// I'm using Visual language format (you can find it in Apple Documentation
// In a few words:
// H:|-0-[redBox]-0-|
// "H" - means horizontal
// "|" - short cut for parent view (in our case it is UIViewController.view)
// "[redBox]" - view name from view's dictionary
// "-0-" - gap between views (you could set number), in our case it is "|" and "[redBox]"
// "[redBox(boxSize)]" - means that view (redBox) size should be qual to "boxSize" from metrics' dictionary
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[redBox]-0-|" options:NSLayoutFormatAlignAllCenterY metrics:metrics views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[redBox(boxSize)]" options:NSLayoutFormatAlignAllCenterY metrics:metrics views:views]];
Apple Documentation
You are setting a UIView with a frame (0, 0, 320, 120). This frame will fit the iPhone 4 screen, as the phone screen width is 320 pixels. But you cant expect same when you run the code in iPhone 6/6s. Setting Autoresizing will not handle this. You need to use constraints/autolayout for that.
Autoresizing masks describe how a subview will resize or move when its superview is resized.
So after adding this view, if you change the phone orientation, this will resize the view in position accordingly. But you need to set the frame according to the superview first. You can set the width dynamically, like: (0, 0, self.view.frame.size.width, 120).

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.

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.

Aligning a view next to another view using FLKAutoLayout

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

Implementing auto layout for views generated programmatically

I have an app whose views are generated programmatically. Example:
-(void)loadView
{
[super loadView];
// SET TOP LEFT BTN FOR NEXT VIEW
UIBarButtonItem *topLeftBtn = [[UIBarButtonItem alloc] initWithTitle:#"Back" style:UIBarButtonItemStyleBordered target:nil action:nil];
self.navigationItem.backBarButtonItem = topLeftBtn;
[topLeftBtn release];
// programmatically set up the view for cart tableView
CGRect iouTableViewFrame = CGRectMake(0, 0, 320, 348);
iouTableView = [[UITableView alloc]initWithFrame:iouTableViewFrame style:UITableViewStylePlain];
[[self iouTableView] setDelegate:self];
[[self iouTableView] setDataSource:self];
[[self view] addSubview:iouTableView];
// set up the summary label
CGRect summaryTableFrame = CGRectMake(0, 348, 320, 18);
UILabel *summaryTableLabel = [[UILabel alloc] initWithFrame:summaryTableFrame];
[summaryTableLabel setFont:[UIFont fontWithName:#"Helvetica" size:14]];
[summaryTableLabel setText:#" Summary"];
UIColor *labelColor = UIColorFromRGB(MiddleBlueColor);
[summaryTableLabel setBackgroundColor:labelColor];
[summaryTableLabel setTextColor:[UIColor whiteColor]];
[[self view] addSubview:summaryTableLabel];
// set up the summary table
CGRect summaryTableViewFrame = CGRectMake(0, 366, 320, 44);
summaryTableView = [[UITableView alloc]initWithFrame:summaryTableViewFrame style:UITableViewStylePlain];
[summaryTableView setScrollEnabled:NO];
[[self summaryTableView] setDelegate:self];
[[self summaryTableView] setDataSource:self];
[[self view] addSubview:summaryTableView];
}
Yes. I will update to NIBs and use interface builder and storyboard in the future but I have not done ios programming in a year.
With the new iPhone 5 having a different screen size, the app just does not look good and I need to implement auto layout of some sort. Is there a way to do it programmatically for now instead of using IB?
Thanks much!
Yes there is, by using two methods in NSLayoutConstraint
-(NSArray*)constraintsWithVisualFormat:options:metrics:views:
-(NSLayoutConstraint*)constraintWithItem:attribute:relatedBy:toItem:attribute:
multiplier:constant:
The visual format language is all packaged up into an NSString
So I'll take your iouTableView for example.
[self.view addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"|[iouTableView]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(iouTableView)]];
The pipe symbol "|" represents the superview's edge.
The [] represent a view.
So what we did there was we hooked the iouTableView's left and right edge to the left and right edge of its superview.
Another example of the visual format:
Let's hook your table view, summary label and summary table vertically.
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:
#"V:|[iouTableView(348)][summaryTableLabel(18)][summaryTableView(44)]"
options:NSLayoutFormatAlignAllLeft
metrics:nil
views:NSDictionaryOfVariableBindings(iouTableView, summaryTableLabel, summaryTableView)]];
Now this links up all three views vertically on each of their edges, NSLayoutFormatAlignAllLeft tells all the views to align left and they'll do so based on other constraints, in this case, the previous constraint.
The ()s are used to specify the size of the views.
There's a bit more like inequalities and priorities as well as the "-" spacer symbol but check out the apple docs for that
Edit: Corrected the examples to use constraintsWithVisualFormat as shown in the method signature.
In addition to Aplle provided methods you can use Parus lib for operating with AutoLayout from code.
For example you will be able to specify:
PVVFL(#"[view1]-20-[view2]").fromRightToLeft.withViews(views).asArray
Instead of
[NSLayoutConstraint constraintsWithVisualFormat:#"[view1]-20-[view2]"
options:NSLayoutFormatDirectionRightToLeft
metrics:nil
views:views]
Also you will be able to group layouts settings, mix VFL and not VFL constraints.
Parus able to prevent common mistakes, differentiate location and parameters constriaints, and provide great auto-completion support.

Resources