Auto Layout on UITableView header - ios

I've been searching for a clear answer on how to add auto layout to a UITableView. So far, my code looks like:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UINib *nib = [UINib nibWithNibName:#"HomeHeaderView" bundle:nil];
UIView *headerView = (UIView *)[nib instantiateWithOwner:self options:nil][0];
[headerView.layer setCornerRadius:6.0];
[headerView setTranslatesAutoresizingMaskIntoConstraints:NO];
// NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(headerView);
// NSMutableArray *headerConstraints = [[NSMutableArray alloc] init];
// [headerConstraints addObject:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[headerView]-|" options:0 metrics:nil views:viewsDictionary]];
// [headerConstraints addObject:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[headerView]-|" options:0 metrics:nil views:viewsDictionary]];
// [self.actionsTableView addConstraints:headerConstraints];
// [self.view addSubview:headerView];
tableView.tableHeaderView = headerView;
[headerView layoutSubviews];
NSLayoutConstraint *centerX = [NSLayoutConstraint constraintWithItem:headerView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0];
NSLayoutConstraint *centerY = [NSLayoutConstraint constraintWithItem:headerView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1 constant:0];
NSLayoutConstraint *width = [NSLayoutConstraint constraintWithItem:headerView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:1 constant:300];
NSLayoutConstraint *height = [NSLayoutConstraint constraintWithItem:headerView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:1 constant:90];
[self.view addConstraints:#[centerX, centerY, width, height]];
return headerView;
}
I basically have a nib file for my header view and I want to center that nib in my UITableViewHeader. I'd like it to grow and shrink accordingly in portrait and landscape orientations. I'm honestly unsure if I set up the constraint properly. I was not sure if my toItem was supposed to be the view controller's view, or the tableview itself.
I also did not know if I was supposed to add the headerview as a subview to either the view controller's view, or the tableview itself.
Or, I wasn't sure if setting tableView.tableHeaderView = headerView was enough.
I really have no clue what the best practices are for something like this. I wasn't sure if it all could be done in IB as well. Currently, with the code you see, I get this error:
'Auto Layout still required after executing -layoutSubviews. UITableView's implementation of -layoutSubviews needs to call super.'
It's because of that error, that I added [headerView layoutSubviews]
Thoughts on this? Thanks in advance!

The real problem is that you've confused viewForHeaderInSection: with the table's headerView. They are unrelated.
The former is the header for a section. You return the view from the delegate method.
That latter is the header for the table. You set the view, probably in your viewDidLoad.
Constraints operate in the normal way. But they should only be internal constraints to their subviews. At the time you form it, the view is not in your interface. And its size and place are not up to you at that time. If it's the section header, it will be resized automatically to fit correctly (in accordance with the table's width and the table's or delegate's statement of the header height). If it's the table header, you can give it an absolute height, but its width will be resized to fit correctly.
Here is a complete example of constructing a section header with internal constraints on its subviews.
- (UIView *)tableView:(UITableView *)tableView
viewForHeaderInSection:(NSInteger)section {
UITableViewHeaderFooterView* h =
[tableView dequeueReusableHeaderFooterViewWithIdentifier:#"Header"];
if (![h.tintColor isEqual: [UIColor redColor]]) {
h.tintColor = [UIColor redColor];
h.backgroundView = [UIView new];
h.backgroundView.backgroundColor = [UIColor blackColor];
UILabel* lab = [UILabel new];
lab.tag = 1;
lab.font = [UIFont fontWithName:#"Georgia-Bold" size:22];
lab.textColor = [UIColor greenColor];
lab.backgroundColor = [UIColor clearColor];
[h.contentView addSubview:lab];
UIImageView* v = [UIImageView new];
v.tag = 2;
v.backgroundColor = [UIColor blackColor];
v.image = [UIImage imageNamed:#"us_flag_small.gif"];
[h.contentView addSubview:v];
lab.translatesAutoresizingMaskIntoConstraints = NO;
v.translatesAutoresizingMaskIntoConstraints = NO;
[h.contentView addConstraints:
[NSLayoutConstraint
constraintsWithVisualFormat:#"H:|-5-[lab(25)]-10-[v(40)]"
options:0 metrics:nil views:#{#"v":v, #"lab":lab}]];
[h.contentView addConstraints:
[NSLayoutConstraint
constraintsWithVisualFormat:#"V:|[v]|"
options:0 metrics:nil views:#{#"v":v}]];
[h.contentView addConstraints:
[NSLayoutConstraint
constraintsWithVisualFormat:#"V:|[lab]|"
options:0 metrics:nil views:#{#"lab":lab}]];
}
UILabel* lab = (UILabel*)[h.contentView viewWithTag:1];
lab.text = self.sectionNames[section];
return h;
}

I found that solution provided by matt might not be the perfect, because he's adding custom views and constraints to UITableViewHeaderFooterView's contentView. That is always causing Auto Layout warnings in runtime: Unable to simultaneously satisfy constraints when we want to have dynamic header height.
I am not sure about the reason, but we can assume that iOS adds some extra constrains to contentView that sets fixed width and height of that view. Warnings generated in runtime tells that constraints we added manually can't be satisfied with those, and it's obvious because our constraints should stretch header view so the subviews can fit in it.
Solution is pretty easy - don't use UITableViewHeaderFooterView's contentView, just add your subviews directly to UITableViewHeaderFooterView. I can confirm that it's working without any issues on iOS 8.1. If you want to add several views and change the background color of you header, consider adding UIView that fills header view (thanks to AutoLayout constraints) and then all the subviews you would like to have to that view (I am calling it customContentView). That way we can avoid any AutoLayout issues and have auto-sizing headers in UITableView.

This is a neat solution:
Optional: initWithStyle:UITableViewStyleGrouped to prevent floating tableViewHeader
Make two properties, the label is just for demonstration:
#property (nonatomic, strong, readwrite) UIView *headerView;
#property (nonatomic, strong, readwrite) UILabel *headerLabel;
Setup everything in viewDidLoad:
self.headerView = [[UIView alloc] initWithFrame:CGRectZero];
self.headerLabel = [[UILabel alloc] init];
self.headerLabel.text = #"Test";
self.headerLabel.numberOfLines = 0; //unlimited
self.headerLabel.textAlignment = NSTextAlignmentCenter;
self.headerLabel.translatesAutoresizingMaskIntoConstraints = NO; //always set this to NO when using AutoLayout
[self.headerView addSubview:self.headerLabel];
NSString *horizontalFormat = #"H:|-[headerLabel]-|";
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:horizontalFormat options:0 metrics:nil views:#{#"headerLabel":self.headerLabel}];
[self.headerView addConstraints:horizontalConstraints];
NSString *verticalFormat = #"V:|-[headerLabel]-|";
NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:verticalFormat options:0 metrics:nil views:#{#"headerLabel":self.headerLabel}];
[self.headerView addConstraints:verticalConstraints];
In viewForHeaderInSection:
return self.headerView;
In heightForHeaderInSection:
self.headerLabel.preferredMaxLayoutWidth = tableView.bounds.size.width;
return [self.headerView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

Related

UIScrollView with Auto Layout Constraints: Auto Content Size Calculation

I'm having troubles with UIScrollView using auto layout constraints. I have the following view hierarchy, with constraints set through IB:
- ScrollView (leading, trailing, bottom and top spaces to Superview)
-- ContainerView (leading, trailing, bottom and top spaces to ScrollView)
--- Button 1 (full width, **top space to ContainerView**)
--- Button 2 (full width, below Button 1)
--- Button n (full width, below Button n-1, **bottom space to ContainerView**)
I want a simple scrollabel list of buttons. Here is my code:
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view setBackgroundColor:[UIColor redColor]];
[self.contentView setBackgroundColor:[UIColor yellowColor]];
UIView *lastView= self.contentView; // use for top constraint
NSInteger topBottomMargin= 10, leftRightMargin= 16;
for (int i=0; i<10; i++) {
UIButton *button= [UIButton buttonWithType:UIButtonTypeSystem];
button.translatesAutoresizingMaskIntoConstraints= NO;
[button setTitle:[NSString stringWithFormat:#"Button %d", i] forState:UIControlStateNormal];
[self.contentView addSubview:button];
// add constraints
// top
[self.contentView addConstraint:[NSLayoutConstraint
constraintWithItem:lastView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationGreaterThanOrEqual
toItem:button
attribute:NSLayoutAttributeTop
multiplier:1.0 constant:-topBottomMargin]];
// left
[self.contentView addConstraint:[NSLayoutConstraint
constraintWithItem:self.contentView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:button
attribute:NSLayoutAttributeLeading
multiplier:1.0 constant:-leftRightMargin]];
// right
[self.contentView addConstraint:[NSLayoutConstraint
constraintWithItem:self.contentView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:button
attribute:NSLayoutAttributeTrailing
multiplier:1.0 constant:leftRightMargin]];
lastView= button;
}
// bottom
[self.contentView addConstraint:[NSLayoutConstraint
constraintWithItem:self.contentView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:lastView
attribute:NSLayoutAttributeBottom
multiplier:1.0 constant:topBottomMargin]];
}
It seems the height of contentView is 0! But there are constraints both for top and bottom of it. It should be like this:
But with my code it's like this. Any Help would be great.
You can add constraint to container view to scroll view as Equal height & equal width. Also when you add constraint to buttons don't forget add bottom constraints to buttons as it will decide the end of scroll view(content size).
Since you are using auto-layout constraints on the contentView, it's height (frame) will be zero in the viewDidLoad method. You should move your code into the viewDidLayoutSubviews method and try to add your buttons there.
You should get the height of the contentView there. Please let me know if that works. Hope this helps.
See this question for reference: iOS AutoLayout - get frame size width
I don't think we can't add auto layout directly to a ContainerView inside ScrollView with Intrinsic Size as Default, so I add ContainerView as subview programmatically:
- (void)viewDidLoad{
[super viewDidLoad];
self.contentView = [[UIView alloc] initWithFrame:CGRectZero];
[self.scrollView addSubview:self.contentView];
//Then add your button here as normal.
//...
}
And Gurtej Singh is right, we have to update the frame in the viewDidLayoutSubviews:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
//Don't for get to update your self.scrollView.contentSize if you want to able to scroll
//...
//Update your contentView frame based on scrollview frame and self.scrollView.contentSize.
self.contentView.frame = self.scrollView.bounds or ....;
}
I just want to help, it might not a good solution, but it work for me.
I found the solution i was looking for here:
[super viewDidLoad];
UIScrollView* sv = [UIScrollView new];
sv.backgroundColor = [UIColor whiteColor];
sv.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:sv];
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[sv]|"
options:0 metrics:nil
views:#{#"sv":sv}]];
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[sv]|"
options:0 metrics:nil
views:#{#"sv":sv}]];
UILabel* previousLab = nil;
for (int i=0; i<30; i++) {
UILabel* lab = [UILabel new];
lab.translatesAutoresizingMaskIntoConstraints = NO;
lab.text = [NSString stringWithFormat:#"This is label %i", i+1];
[sv addSubview:lab];
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(10)-[lab]"
options:0 metrics:nil
views:#{#"lab":lab}]];
if (!previousLab) { // first one, pin to top
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-(10)-[lab]"
options:0 metrics:nil
views:#{#"lab":lab}]];
} else { // all others, pin to previous
[sv addConstraints:
[NSLayoutConstraint
constraintsWithVisualFormat:#"V:[prev]-(10)-[lab]"
options:0 metrics:nil
views:#{#"lab":lab, #"prev":previousLab}]];
}
previousLab = lab;
}
// last one, pin to bottom and right, this dictates content size height
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:[lab]-(10)-|"
options:0 metrics:nil
views:#{#"lab":previousLab}]];
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:[lab]-(10)-|"
options:0 metrics:nil
views:#{#"lab":previousLab}]];
// look, Ma, no contentSize!

Autolayout Issue in UIView

I am here to get help for a simple problem.My app runs in both 3.5" and 4" screens but the view is not centred in both the sizes(check images for the view).I am using auto layout and also tried to reset suggested constraints.When i bring the label to centre in one view it is not positioned correctly in the other view.Please help i am stuck
You need to add 4 constraints to the view which needs to be centered.
Pin:
1. Width.
2. Height.
Align:
3. Horizontal center in container.
4. Vertical center in container.
Wrap all your subviews except the red grandient background inside a container UIView:
#property (nonatomic, strong) UIView *container;
[self.container addSubview:greyBox];
[self.container addSubview:getStarted];
...
[self.view addSubview:self.redBG];
[self.view addSubview:self.container];
Update your constraints to be relative to your container view instead of self.view.
Then center your container view to your self.view:
// ---------------------------------------------------------------
// centers the container view to your view controller's vivew
// ---------------------------------------------------------------
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.container attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.container attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]];
That way, your subview always appear to be centered.
I think the problem you're having at the moment is that you 'offset' each subview's y position relative to the self.view's top edge. You might get it right on one screen, but then when your screen gets longer, it appears wrong.
Using the above view container method, lets you dynamically add as much subviews as you like and it is still centered, assuming your subviews are connected to the container view's top and bottom edges like a tent.
Floating subviews inside a container view will cause the container to have 0 height and not work.
Update
Not sure how you've setup your IB constraints, you might want to show screenshots of them.
Perhaps you have not setup your constraints properly in IB. Maybe you can improve your constraint settings similar to the ones I've provided below to see if it fixes it.
This is a demo of the method I mentioned above:
4 inch iPhone screen:
3.5 inch iPhone screen:
Notice how both screenshot shows the white box being centered in the view.
This is the code I used:
Header File
// HEADER FILE
#import <UIKit/UIKit.h>
#interface MainViewController : UIViewController
#property (nonatomic, strong) UIView *gradient;
#property (nonatomic, strong) UIView *container;
#property (nonatomic, strong) UIView *whiteBox;
#property (nonatomic, strong) UILabel *label1;
#property (nonatomic, strong) UIButton *getStarted;
#property (nonatomic, strong) UILabel *label2;
#end
Implementation File
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
[self initView];
[self initConstraints];
}
-(void)initView
{
self.gradient = [[UIView alloc] init];
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.view.frame;
UIColor *redColor = [UIColor colorWithRed:157.0/255.0 green:37.0/255.0 blue:29.0/255.0 alpha:1.0];
UIColor *pinkColor = [UIColor colorWithRed:216.0/255.0 green:101.0/255.0 blue:100.0/255.0 alpha:1.0];
gradientLayer.startPoint = CGPointMake(0.5, 0.0);
gradientLayer.endPoint = CGPointMake(0.5, 1.0);
gradientLayer.colors = #[(id)redColor.CGColor,
(id)pinkColor.CGColor,
(id)redColor.CGColor];
[self.gradient.layer insertSublayer:gradientLayer atIndex:0];
// container view
self.container = [[UIView alloc] init];
// white box
self.whiteBox = [[UIView alloc] init];
self.whiteBox.backgroundColor = [UIColor colorWithRed:0.95 green:0.95 blue:0.95 alpha:1.0];
self.whiteBox.layer.shadowColor = [UIColor blackColor].CGColor;
self.whiteBox.layer.shadowOffset = CGSizeMake(0.0, 3.0);
self.whiteBox.layer.shadowOpacity = 0.5;
self.whiteBox.layer.shadowRadius = 3.0;
self.label1 = [[UILabel alloc] init];
self.label1.text = #"Label 1 Text Goes Here";
self.label1.textAlignment = NSTextAlignmentCenter;
self.label1.textColor = redColor;
self.getStarted = [[UIButton alloc] init];
[self.getStarted setTitle:#"Get Started" forState:UIControlStateNormal];
[self.getStarted setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
self.getStarted.backgroundColor = redColor;
self.label2 = [[UILabel alloc] init];
self.label2.text = #"Version 1.3 (log enabled)";
self.label2.textAlignment = NSTextAlignmentCenter;
self.label2.textColor = [UIColor darkGrayColor];
self.label2.font = [UIFont fontWithName:#"Arial" size:14];
[self.whiteBox addSubview:self.label1];
[self.whiteBox addSubview:self.getStarted];
[self.whiteBox addSubview:self.label2];
[self.container addSubview:self.whiteBox];
[self.view addSubview:self.gradient];
[self.view addSubview:self.container];
}
-(void)initConstraints
{
self.gradient.translatesAutoresizingMaskIntoConstraints = NO;
self.container.translatesAutoresizingMaskIntoConstraints = NO;
self.whiteBox.translatesAutoresizingMaskIntoConstraints = NO;
self.label1.translatesAutoresizingMaskIntoConstraints = NO;
self.getStarted.translatesAutoresizingMaskIntoConstraints = NO;
self.label2.translatesAutoresizingMaskIntoConstraints = NO;
id views = #{
#"gradient": self.gradient,
#"container": self.container,
#"whiteBox": self.whiteBox,
#"label1": self.label1,
#"getStarted": self.getStarted,
#"label2": self.label2
};
// gradient constraint
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[gradient]|" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[gradient]|" options:0 metrics:nil views:views]];
// container constraitns
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.container
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.container
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0.0]];
// white box constraints
[self.container addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-10-[whiteBox(280)]-10-|" options:0 metrics:nil views:views]];
[self.container addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-10-[whiteBox]-10-|" options:0 metrics:nil views:views]];
// white box subview constraints
[self.whiteBox addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-10-[label1]-10-|" options:0 metrics:nil views:views]];
[self.whiteBox addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-10-[getStarted]-10-|" options:0 metrics:nil views:views]];
[self.whiteBox addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-10-[label2]-10-|" options:0 metrics:nil views:views]];
[self.whiteBox addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-10-[label1]-20-[getStarted(40)]-15-[label2]-5-|" options:0 metrics:nil views:views]];
}

How to resize custom UITableView separators on landscape and prevent from disappearing

I've decided to programmatically create my own UITableView separator lines because I need fine control over displaying a separator above and/or below each individual UITableViewCell. My tableView has static cells, so I do not create the separators in cellForRowAtIndexPath. Instead, I have propertys for each cell and in viewDidLoad, I add a top and/or bottom separator as needed. It's working, until I rotate to landscape and then the separator line does not stretch to fill the screen - it of course remains the same width it was when created. I'm not seeing how I can automatically adjust them to fit the width of the screen.
I tried adding Auto Layout constraints (leading, trailing, top/bottom), but for some reason it's not working - the width does not change, but there are no error messages logged to indicate anything is wrong with the constraints. The separator lines also sometimes disappear upon scroll or rotate, and if I comment out the auto layout constraints then they do not disappear.
So how can I make my custom cell separators always stretch to fill the device width upon rotation, and how do I prevent them from disappearing?
If it would be easier/better to create my custom cell separators in a different way, I am willing to do that. I just don't know how this can be done aside from my approach when the cells are static. I considered creating the views in the Storyboard, and setting up the constraints visually, but would that not be the equivalent of what I'm doing programmatically? If they were dynamic cells I would do it in cellForRowAtIndexPath.
//In viewDidLoad:
[self addTopSeparatorForCell:self.myCell];
//Helper method
- (void)addTopSeparatorForCell:(UITableViewCell *)cell {
UIView *topSeparator = [[UIView alloc] initWithFrame:CGRectMake(15, 1, cell.contentView.frame.size.width, 0.5)];
//add CALayer to preserve line separator visibility when row is highlighted
CALayer *backgroundColorLayer = [CALayer layer];
backgroundColorLayer.frame = topSeparator.bounds;
backgroundColorLayer.backgroundColor = [UIColor colorWithWhite:204/255.0f alpha:1].CGColor;
[topSeparator.layer addSublayer:backgroundColorLayer];
[cell.contentView addSubview:topSeparator];
//add auto layout constraints
topSeparator.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *cn = nil;
cn = [NSLayoutConstraint constraintWithItem:topSeparator
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:cell.contentView
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:15];
[cell.contentView addConstraint:cn];
cn = [NSLayoutConstraint constraintWithItem:topSeparator
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:cell.contentView
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:0];
[cell.contentView addConstraint:cn];
cn = [NSLayoutConstraint constraintWithItem:topSeparator
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:cell.contentView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:1];
[cell.contentView addConstraint:cn];
}
EDIT: Thanks to # user1966109, we've been able to solve the issue with the lines not extending to fill the width, and now they are preserved when highlighting a cell. But one issue still remains that I haven't been able to solve, since I'm not sure why it's occurring. The separator lines disappear after scrolling down the scrolling back up. It's related to the auto layout constraints though because a previous solution which had other issues did not exhibit this problem. Here's the current solution that causes the lines to disappear. I'd appreciate it if someone knows how to prevent this problem (and preserve the other issues already resolved).
[self.cell addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(15#750)-[myView]-(-47#750)-|" options:0 metrics:0 views:viewsDictionary]];
[self.cell addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[myView(2)]-(-2#750)-|" options:0 metrics:0 views:viewsDictionary]];
You should not mix initWithFrame and Auto Layout. You can have a good result with a few lines using Visual Format Language for Auto layout:
//#interface TableViewController ()
#property (weak, nonatomic) IBOutlet UITableViewCell *cell;
//#implementation TableViewController
- (void)viewDidLoad
{
[super viewDidLoad];
UIView *myView = [[UIView alloc] init];
myView.backgroundColor = [UIColor redColor];
[self.cell.contentView addSubview:myView];
myView.translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(myView);
[self.cell.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[myView]|" options:0 metrics:0 views:viewsDictionary]];
[self.cell.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[myView(2)]" options:0 metrics:0 views:viewsDictionary]];
}
This handles rotation perfectly.
Edit!
Set the following constraints if using a accessory view:
//Set a negative value to the trailing space in order to display myView under the accessory view
//Those constraints work for both self.cell and self.cell.contentView (kind of odd)
[self.cell addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(15#750)-[myView]-(-47#750)-|" options:0 metrics:0 views:viewsDictionary]];
[self.cell addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[myView(2)]-(-2#750)-|" options:0 metrics:0 views:viewsDictionary]];
With the initial help of user1966109, I have figured out constraints that address all of the problems and are working well:
[cell addConstraint:[NSLayoutConstraint constraintWithItem:imageView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:cell
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:indent]];
[cell addConstraint:[NSLayoutConstraint constraintWithItem:imageView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:cell
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:0.0]];
[cell addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[imageView(0.5)]" options:0 metrics:0 views:viewsDictionary]];

Programmatically creating "spacing to nearest neighbor" constraint

Part of the screen I'm building includes a section with n views. I'm generating these views on the fly in code--they're simple UIView subclasses.
I'm using AutoLayout constraints for this screen, and I'd like each view to automatically position itself 15px or so below the view above it.
In Xcode it's possible to create a spacing to nearest neighbor constraint, which seems to do exactly what I want.
However, I can't seem to find any examples that show how to create this in code.
Is it possible to create a "spacing to nearest neighbor" constraint programmatically?
I was actually doing some personal exercises of auto-layout and animations when I stumbled over your question and decided to extend my a small demo, which you can download here.
If I have understood you correctly below piece of code can be used for inspiration. With a small effort it could be extended with removal of views too and also with dynamic height of the views.
Please note my code includes a solution with and without animation - the latter is of course more simple.
#import "Demo2ViewController.h"
#interface Demo2ViewController ()
{
NSMutableArray *_viewList;
NSDictionary *_metrics;
}
#end
#implementation Demo2ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
[[self view] setBackgroundColor:[UIColor colorWithRed:.95 green:.95 blue:.95 alpha:1.0]];
_metrics = #{#"height": #30, // height of the views being added
#"space": #15}; // space between two views
// the first view
UITextView *textView = [[UITextView alloc] initWithFrame:CGRectNull];
_viewList = [[NSMutableArray alloc] initWithObjects:textView, nil];
textView.text = [NSString stringWithFormat:#"view: %lu", (unsigned long)[_viewList count]];
// a button to add more views
UIButton *buttonAddView = [[UIButton alloc] initWithFrame:CGRectNull];
[buttonAddView setTitle:#"add new view" forState:UIControlStateNormal];
[buttonAddView setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[buttonAddView addTarget:self action:#selector(buttonPushed:) forControlEvents:UIControlEventTouchDown];
NSDictionary *subviews = NSDictionaryOfVariableBindings(textView, buttonAddView);
for (id view in [subviews allValues]) {
[[self view] addSubview:view];
[view setTranslatesAutoresizingMaskIntoConstraints:NO];
}
// initial constraints
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[textView]-|" options:0 metrics:nil views:subviews]];
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[buttonAddView]-|" options:0 metrics:nil views:subviews]];
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[textView(==height)]" options:0 metrics:_metrics views:subviews]];
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[buttonAddView]-|" options:0 metrics:nil views:subviews]];
}
-(void)buttonPushed:(UIButton*)button
{
UITextView *prevView = [_viewList lastObject]; // get reference to previous view
// create a new view
UITextView *newView = [[UITextView alloc] initWithFrame:CGRectNull];
[[self view] addSubview:newView];
[newView setTranslatesAutoresizingMaskIntoConstraints:NO];
[_viewList addObject:newView];
newView.text = [NSString stringWithFormat:#"view: %lu", (unsigned long)[_viewList count]];
NSDictionary *subviews = NSDictionaryOfVariableBindings(prevView, newView);
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[newView]-|" options:0 metrics:nil views:subviews]];
#if 0
// without animation
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[prevView]-space-[newView(==height)]" options:0 metrics:_metrics views:subviews]];
[[self view] layoutIfNeeded];
#else
// with animation
// to begin with the new view gets zero height and no space to previous view
NSArray *tempConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[prevView][newView(==0)]" options:0 metrics:nil views:subviews];
[[self view] addConstraints:tempConstraints];
[[self view] layoutIfNeeded]; // to ensure zero height is the starting point for the animation
[newView setAlpha:0.0f]; // starting point for fade-in
[UIView animateWithDuration:0.25f animations:^{
[[self view] removeConstraints:tempConstraints]; // remove zero height constraint
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[prevView]-space-[newView(==height)]" options:0 metrics:_metrics views:subviews]]; // add final constraints
[newView setAlpha:1.0f]; // fade-in
[[self view] layoutIfNeeded];
}];
#endif
}
#end
You can create a constraint dictionary and a constraint string and apply them programmatically.
Adding items and keys to the dictionary is trivial so I won't deal with that. Just remember every view in the constraint system must be in the dictionary.
Creating the format string is the interesting bit. Presumably you want to add your views under as certain view, say it has the NSString key topView. The first part of the format string looks like
NSString *constraintBase = [NSString stringWithFormat:#"V:topView"];
For each view you want to add, you add to that string
NSString *constraintString = [constraintBase stringByAppendingString:[NSString stringWithFormat:#"-15-%#", viewDictionaryKey]];
Finally apply constraintString as usual, a constructed visual constraint format string.
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:constraintString options:0 metrics:nil views:viewsToConstrain]];
Here the view's which are generated programatically are added to a parent view.
When a view is created its constraints should be added to it.
But the constraints must mapped with the previous view, and so we need to identify the previous view (last view in the parent view)
NSArray *subViewList = [_vwParentView subviews];
UIView *lastView;
if (subViewList.count > 0) {
lastView = [subViewList lastObject];
}
The above code will help to find the last created view.
When Views are created programatically and added as sub views, the views will added as stack for parrentView and hence the view created at last will be the last object in the subViewList array
Note: Assuming that a separate view is assigned as parent view with no subviews initially.
UIView *contentView = [[UIView alloc]init];
NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeWidth multiplier:1.0 constant:initialWidth];
[contentView addConstraint:widthConstraint];
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:contentView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeHeight multiplier:1.0 constant:initialHeight];
[contentView addConstraint:heightConstraint];
contentView.translatesAutoresizingMaskIntoConstraints = NO;
[_vwParentView addSubview:contentView];
NSLayoutConstraint *gapMaintainTopConstraint;
if (lastView == nil) {
gapMaintainTopConstraint = [NSLayoutConstraint constraintWithItem:contentView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:_vwParentView attribute:NSLayoutAttributeTop multiplier:1.0 constant:15];
}
else
{
gapMaintainTopConstraint = [NSLayoutConstraint constraintWithItem:contentView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:lastView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:15];
}
[_vwParentView addConstraint:gapMaintainTopConstraint];
In case to change the size of the added view in future, it should be achieved by changing its widthConstraint or heightConstraint, only then the constraint which is associated to it (to maintain specific gap) will work. Size should not be changed using frames thereafter.
Meaning - constraint based views should be handled using constraints only.
It is possible to create a "spacing to nearest neighbour" constraint only after the created view is added as a sub view,.
The translatesAutoresizingMaskIntoConstraints property of the created view should be disabled, so that there won't be any conflict of constraints when there is any change in size (of the created view) in future.

iOS add constraint to table section header

I am working with a grouped table, and am customizing the header in the sections using tableView: viewForHeaderInSection: method and setting the height using tableView: heightForHeaderInSection:.
I created a view and placed a label in it like so:
- (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
// Create a custom title view.
UIView *ctv;
UILabel *titleLabel;
// Set the name.
{...} // Code not relevant
// If an iPhone.
if ([Config isPhone]) {
ctv = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 50)];
titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 14, 320, 36)];
}
// If an iPad
else {
ctv = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 768, 75)];
titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 20, 544, 55)];
}
// Config the label.
{...} // Code not relevant
// Center the items.
ctv.center = CGPointMake(tableView.center.x, ctv.center.y);
titleLabel.center = ctv.center;
// Add the label to the container view.
[ctv addSubview:titleLabel];
// Return the custom title view.
return ctv;
}
This all works great until you rotate the screen. The position is off. I realize that this is because the view is being centered while it is in the other orientation causing the calculation of the center to no longer be correct. The solution should be to add a constraint. I tried adding the constraint below:
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(ctv);
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|[ctv]|"
options:0
metrics:nil
views:viewsDictionary
];
[tableView addConstraints:constraints];
But when I do this trying the method below, I get that no parent view is associated with it, which makes complete sense, because it doesn't technically get added into the view is returned. So I thought I would try to add the constraint this way:
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:ctv
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:tableView
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0
];
[tableView addConstraint:constraint];
But this one also errors. I've tried switching the tableView variables to the global table property but it gives the same results. I also tried to figure out how to add the constraint in the view did load method but it failed as I could not figure out how to get back to the table's section headers from the table object. The last thought I had was to set the width on the table in a constraint and set one to center the entire table. This process worked but now I have the an ugly scroll in the middle of my app when it is in the landscape orientation. So the question is, where/how can I access the individual section headers after they have been loaded to add this constraint? I'm still pretty new to Objective-C so any help is appreciated.
***** NEW CODE BASED FROM rdelmar SUGGESTION ****
- (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UIView *ctv = [tableView dequeueReusableHeaderFooterViewWithIdentifier:#"groupHeader"];
UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 20, 544, 55)];
[titleLabel setTranslatesAutoresizingMaskIntoConstraints: NO];
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:ctv
attribute:NSLayoutAttributeCenterX
relatedBy:0
toItem:titleLabel
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0
];
[ctv addConstraints:#[constraint]];
titleLabel.text = #"string";
[ctv addSubview:titleLabel];
return ctv;
}
But like I mentioned, it is giving me a "Constraint must contain a first layout item" error.
I did it this way in a recent project to add a label and a UIActivityIndicatorView:
-(UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UIView *headerView = [self.tableView dequeueReusableHeaderFooterViewWithIdentifier:#"Header"];
UILabel *label = [[UILabel alloc]initWithFrame:CGRectMake(0,0, 250, 20)];
[label setTranslatesAutoresizingMaskIntoConstraints:NO];
NSLayoutConstraint *con1 = [NSLayoutConstraint constraintWithItem:headerView attribute:NSLayoutAttributeCenterY relatedBy:0 toItem:label attribute:NSLayoutAttributeCenterY multiplier:1 constant:0];
NSLayoutConstraint *con2 = [NSLayoutConstraint constraintWithItem:headerView attribute:NSLayoutAttributeLeft relatedBy:0 toItem:label attribute:NSLayoutAttributeLeading multiplier:1 constant:-10];
[headerView addConstraints:#[con1,con2]];
label.text = #"Pick an Activity from the List";
label.backgroundColor = [UIColor clearColor];
[headerView addSubview:label];
spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
[spinner setTranslatesAutoresizingMaskIntoConstraints:NO];
if (activityIndicatorShouldStop == NO) [spinner startAnimating];
[headerView addSubview:spinner];
NSLayoutConstraint *con3 = [NSLayoutConstraint constraintWithItem:headerView attribute:NSLayoutAttributeCenterY relatedBy:0 toItem:spinner attribute:NSLayoutAttributeCenterY multiplier:1 constant:0];
NSLayoutConstraint *con4 = [NSLayoutConstraint constraintWithItem:headerView attribute:NSLayoutAttributeRight relatedBy:0 toItem:spinner attribute:NSLayoutAttributeTrailing multiplier:1 constant:10];
[headerView addConstraints:#[con3,con4]];
return headerView;
}
If you can't get the constraints to work, your original code supplemented by an autoresizing mask (flexible left and right margins) would do the job.
An even simpler solution would be to return a UILabel as the header view, with centered text.
Your first attempt at constraints wouldn't work because you are setting them up wrong.
The table view is responsible for setting the frame of the header view. You need to worry about the position of the label within the header. The VFL for this would be "|titleLabel|" - the title label should be sized to its superview, the header view.

Resources