Implementing auto layout for views generated programmatically - ios

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.

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).

How to create three views using autolayout, with one fix width height and other two growing height

I am trying to create a view setup vertically where i have one UIView (fixed width, height) at the top and two UILabels (fixed width, dynamic height) at the bottom. Padding all around the view (aView) is 5. Padding all around of _mylabel is 5. Padding on left and right of _yourLable is 5. _yourLable will grow as based on text, but when text content is too large, it will just stop to grow for maintain padding from superview of 5.
This is what i have tried:
- (void)viewDidLoad {
[super viewDidLoad];
UIView *superview = self.view;
UIView *aView = [UIView new];
[aView setBackgroundColor:[UIColor blackColor]];
[aView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:aView];
_mylabel = [[UILabel alloc]init];
[_mylabel setNumberOfLines:0];
[_mylabel setBackgroundColor:[UIColor redColor]];
[_mylabel setTranslatesAutoresizingMaskIntoConstraints:NO];
_mylabel.text = #"i am trying to create a view setup vertically where i have one UIView(fix width, height) at top and other two UILables(fix width, dynamic height) at bottom respectively. Padding on allaround of view/lables is 5. this is what i have tried:";
[self.view addSubview:_mylabel];
_yourLable = [[UILabel alloc]init];
[_yourLable setNumberOfLines:0];
[_yourLable setBackgroundColor:[UIColor redColor]];
[_yourLable setTranslatesAutoresizingMaskIntoConstraints:NO];
_yourLable.text = #"i am trying to create a view setup vertically where i have one UIView(fix width, height) at top and other two UILables(fix width, dynamic height) at bottom respectively. Padding on allaround of view/lables is 5. this is what i have tried:";
[self.view addSubview:_yourLable];
NSDictionary * views = NSDictionaryOfVariableBindings(aView,_mylabel, _yourLable, superview);
NSArray * heightConstraintforLabel = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-5-[aView(==200)]-5-[_mylabel]-5-[_yourLable]-5-|" options:0 metrics:nil views:views];
NSArray * widthConstraintforView = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-5-[aView]-5-|" options:0 metrics:nil views:views];
NSArray * widthConstraintformylabel = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-5-[_mylabel]-5-|" options:0 metrics:nil views:views];
NSArray * widthConstraintforyourLable = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-5-[_yourLable]-5-|" options:0 metrics:nil views:views];
[superview addConstraints:heightConstraintforLabel];
[superview addConstraints:widthConstraintforView];
[superview addConstraints:widthConstraintformylabel];
[superview addConstraints:widthConstraintforyourLable];
}
and
-(void)viewWillLayoutSubviews{
[super viewWillLayoutSubviews];
// Your layout logic here
CGFloat availableLabelWidth = _mylabel.frame.size.width;
_mylabel.preferredMaxLayoutWidth = availableLabelWidth;
availableLabelWidth = _yourLable.frame.size.width;
_yourLable.preferredMaxLayoutWidth = availableLabelWidth;
}
This is what i am getting, without warnings:
I want both labels to resize based on exact text height.
I want last red label to grow as per text written in it, but never go beyond bottom space of 5. That is it should grow but maintain bottom padding of 5.
I have tried various combination with vertical content compression for labels..., but not got exact solution.
Help :)
Just in case you have not set the priorities, use
[_mylabel setContentCompressionResistancePriority:751 forAxis:UILayoutConstraintAxisVertical];
[_mylabel setContentHuggingPriority:252 forAxis:UILayoutConstraintAxisVertical];
You don't need to set any priorities for _yourLable.
And you don't need to set the preferredMaxLayoutWidth for any of the labels in viewWillLayoutSubviews, hence you don't need to override viewWillLayoutSubviews. You can comment the whole method.
Verified on iOS 7 and iOS 9 devices.
Simulator screenshot looks like this,

Auto layout of right-aligned view not working

I am trying to adapt this tableview section header view to auto layout so it will adapt for iPhone 6 and 6 Plus. This is the desired result
which has 3 views named stringView, favoriteCount and heartImage. The following screenshots will have a grey background set on each view to make it easier to see what is going on.
What I don't understand is that the following
#"|-(15.0)-[stringView]-(>=20.0)-[favoriteCount(44.0)]-[heartImage(22.0)]-(10.0)-|"
produces
with the count and heart views nowhere to be seen. So it feels as if the containing view is far wider than it needs to be, but I'm not aware that I have any control over that as the returned view from this method should automatically be sized to the tableview width shouldn't it?
BTW This is all happening inside the tableView:viewForHeaderInSection: method which looks like
- (UIView *)tableView:(UITableView*)tableView viewForHeaderInSection:(NSInteger)section {
if (section == 0) {
// My Favourites menu
UIView *myFavoritesMenuView = [[UIView alloc] init];
[myFavoritesMenuView setBackgroundColor:[UIColor colorWithWhite:0.2 alpha:1.0]];
UILabel *stringView = [[UILabel alloc] init];
[stringView setTranslatesAutoresizingMaskIntoConstraints:NO];
[stringView setText:#"My Favourites"];
[stringView setTextColor:[UIColor whiteColor]];
[stringView setFont:[UIFont fontWithName:#"OpenSans" size:16.0]];
[stringView setBackgroundColor:[UIColor grayColor]];
[myFavoritesMenuView addSubview:stringView];
NSDictionary *favoriteArticles = [[NSUserDefaults standardUserDefaults] dictionaryForKey:#"FavoriteArticles"];
UILabel *favoriteCount = [[UILabel alloc] init];
[favoriteCount setTranslatesAutoresizingMaskIntoConstraints:NO];
[favoriteCount setFont:[UIFont fontWithName:#"OpenSans" size:16.0]];
[favoriteCount setTextColor:[UIColor whiteColor]];
[favoriteCount setTextAlignment:NSTextAlignmentRight];
[favoriteCount setBackgroundColor:[UIColor grayColor]];
[myFavoritesMenuView addSubview:favoriteCount];
if (favoriteArticles && favoriteArticles.count > 0) {
[favoriteCount setText:[NSString stringWithFormat:#"%lu", (unsigned long)favoriteArticles.count]];
} else {
[favoriteCount setText:#""];
}
UIImageView *heartImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"heart-button-on"]];
[heartImage setTranslatesAutoresizingMaskIntoConstraints:NO];
[heartImage setBackgroundColor:[UIColor grayColor]];
[myFavoritesMenuView addSubview:heartImage];
// Create the views dictionary
NSDictionary *views = NSDictionaryOfVariableBindings(stringView, favoriteCount, heartImage);
// Horizontal layout - note the options for aligning the vertical center of all views
NSString *horizontalFormat = #"|-(15.0)-[stringView]-(>=20.0)-[favoriteCount(44.0)]-[heartImage(22.0)]-(10.0)-|";
[myFavoritesMenuView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:horizontalFormat
options:NSLayoutFormatAlignAllCenterY
metrics:nil
views:views]];
// Vertical layout - we only need one "column" of information because of the alignment options used when creating the horizontal layout
NSString *verticalFormat = #"V:|-[stringView]-|";
[myFavoritesMenuView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:verticalFormat
options:0
metrics:nil
views:views]];
return myFavoritesMenuView;
}
// handle other section headers here
}
Incidentally, I've also tried this by creating a UIView as separate xib, containing just the text label aligned left and heart image aligned to trailing edge of superview and am seeing exactly the same issue with the heart not visible (presumably off screen to the right).
The width of the entire table view is incorrect (the table view is square for some reason rather than fitting to the screen) so the missing subviews are off screen to the right. Discovered it when I changed to run in iPhone 6 in landscape view and the subviews appeared...

How do I make my superview resize to match the size of its subviews with Autolayout?

I've got a UIView ("superview") that has a couple of UILabels as subviews, set up in Interface Builder. I've got Auto-Layout turned on to properly space all the labels in a list, one after another. This works well.
What I'm trying to do now is make it so that my Superview resizes vertically to match the height of all my labels and I can't quite figure out how to do this.
How does this work?
Here's an example using visual layouts... so ignore the translatesAutoresizingMaskIntoConstraints business.
This relies on the fact that the container view does NOT have any height constraints and it seems to rely on the spacing between views. The system will resize the view to match the requirements of the subviews.
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor redColor];
[self setTranslatesAutoresizingMaskIntoConstraints:NO];
_labelOne = [[UILabel alloc] init];
_labelOne.text = #"Banana";
_labelOne.backgroundColor = [UIColor blueColor];
[_labelOne setTranslatesAutoresizingMaskIntoConstraints:NO];
_labelTwo = [[UILabel alloc] init];
_labelTwo.text = #"Dinosaur";
_labelTwo.backgroundColor = [UIColor yellowColor];
[_labelTwo setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:_labelOne];
[self addSubview:_labelTwo];
NSDictionary *views = NSDictionaryOfVariableBindings(_labelOne, _labelTwo);
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_labelOne]|" options:0 metrics:nil views:views]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_labelTwo]|" options:0 metrics:nil views:views]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-10-[_labelOne(30)]-15-[_labelTwo(30)]-10-|" options:0 metrics:nil views:views]];
}
return self;
}
I know the basic idea is to have the intrinsic content size of your subviews drive the height of superview by making sure the content compression resistance and content hugging constraints in the vertical dimension for each subview are not being overridden by higher-priority constraints you have added.
Try use the UIView method sizeToFit?

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.

Resources