How to properly constrain UITableView using Auto Layout - ios

I want to place a UITableView such that it takes up the entirety of one of my view controller's subviews. Here's the view hierarchy, where self is my UIViewController subclass:
[self.view] -> [self.rootView] -> [self.tableView]
I know that I need to set setTranslatesAutoresizingMaskIntoConstraints to NO for any view whose subviews I want to lay out with Auto Layout, so here's my viewDidLoad.
[super viewDidLoad];
self.rootView = [[UIView alloc]initWithFrame:CGRectMake(0.0, 0.0, 320.0, 568.0)];
[self.rootView setTranslatesAutoresizingMaskIntoConstraints:NO];
So far so good. Now I want to add a UITableView.
self.tableView = [[UITableView alloc]init]; // same thing with initWithFrame:CGRectZero
[self.rootView addSubview:self.tableView];
No problem so far, but I can't see the table view (because it doesn't have a frame). Let's add some constraints:
[self.rootView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-0-[table]-0-|" options:0 metrics:nil views:#{#"table": self.tableView}]];
[self.rootView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[table]-0-|" options:0 metrics:nil views:#{#"table": self.tableView}]];
Everything breaks. self.rootView.backgroundColor is green and the cells are blue and full of text, but there's nothing on the screen but grey, maybe indicating that self.view is no longer in the view hierarchy? I've done a bunch of googling but can't find a real solution anywhere. I've also tried adding all the constraints one at a time the other way and the same thing happens. Does anyone have any ideas?

I believe you should be calling setTranslatesAutoresizingMaskIntoConstraints:NO on your UITableView instead of on self.rootView. But as rdelmar commented, first make sure your table view is not what you are seeing with the grey background since it will fill the screen and cover the other views.
And another note, I believeself.rootView = [[UIViewController alloc]initWithFrame...] above actually should be [[UIView alloc] initWithFrame...] since controllers don't have frames but maybe it's just incorrect above as this would cause an error when trying to run.

Related

Adding subview to UITableView with autolayout doesn't set its frame

I am using the following code to add a overlay view to a view:
UIView *overlayView = [[UIView alloc] init];
overlayView.backgroundColor = [UIColor redColor];
[self.view addSubview:overlayView];
overlayView.translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary *views = NSDictionaryOfVariableBindings(overlayView);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[overlayView]|" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[overlayView]|" options:0 metrics:nil views:views]];
If I place this code in the viewDidLoad method of a UIViewController it works fine. However, if I do the same in a UITableViewController the overlay view gets a zero frame.
I have inspected the view and I can se the constraints do get added correctly and they are active. But for som reason they seem to be ignored.
I don't get any error or warning.
What am I missing here?
PS: I know I can instantiate the overlay view with self.view.bounds as frame, and it works. However, I need to use autolayout.
Suspect it's because the UITableView is a UIScrollView and auto layout has a few caveats when working with views that establish their own bounds systems (such as the scroll view or table view).
The constraints set on the overlay view are such that it is constrained to the top, right, bottom, and left edges of the scroll view, and thus to the content area of the scroll view rather than the scroll view's frame.
See
https://developer.apple.com/library/ios/technotes/tn2154/_index.html
and Auto Layout Guide: Working with Scroll Views:
Constraints between the edges or margins of the scroll view and its
content attach to the scroll view’s content area.
Instead of constraining overlay view to the content area, constrain it to the scroll view's frame instead. According to the above references, this can be done either by constraining overlay view's width/height to the scroll view, or by constraining the overlay view to a view outside of the scroll view.

shifting down the UITableViewCells

I have a UITableView, and inside it is a UIView, and dynamic UITableViewCells.
The UIView is positioned in the top of the Table, and the cells come right below it.
My problem is, when I enlarge the UIView after a certain button press, it overlaps the cells below it. Due to the cells being dynamic, I am not sure how to access the cells and move their origin.y downwards.
Furthermore, if I have a lot of cells, I don't really want to be moving each cell one by one.
I tried looking to see if the UITableView has some kind of offset parameter for its cells, but didn't find anything of that sort.
Through the storyboard, when I enlarge the UIView inside the table, the cells automatically are pushed below it, but unfortunately when I enlarge the UIView programatically through the code (after the button press), the cells stay in their original place and don't move.
Here is an illustration to show what is happening:
My AutoLayout is turned off.
It sounds like you want your custom UIView to behave like a UITableViewCell by moving the cells up and down as it grows in height. So do just that. Create a custom UITableViewCell whose height can change dynamically and render that cell at the top (or wherever else you want it).
You should not be putting a UIView inside a UITableView in the manner that you are doing. As I see it, there are two main approaches:
1) Add the UIView above the UITableView. You have mentioned no reason as to why it needs to be inside of it.
UIView *view = [[UIView alloc] init];
UITableView *tableView = [[UITableView alloc] init];
view.translateAutoResizingMaskIntoConstraints = NO;
tableView.translateAutoResizingMaskIntoConstraints = NO;
[self.view addSubview:view];
[self.view addSubview:tableView];
NSDictionary *viewsDictionary = #{#"view" : view, #"tableView" : tableView};
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[view]" options:kNilOptions metrics:nil viewDictionary:viewsDictionary];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[view][tableView]|" options:NSLayoutFormatAlignAllLeading | NSLayoutFormatAlignAllTrailing metrics:nil viewDictionary:viewsDictionary];
2) Assuming the UIView absolutely needs to be inside the UITableView, the best way to do it is by using the contentOffset.
CGSize innerViewSize = CGSizeMake(CGRectGetWidth(self.view.bounds), 200.0f);
UIView *innerView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, -innerViewSize.height, innerViewSize.width, innerViewSize.height)];
UITableView *tableView = [[UITableView alloc] initWithStyle:UITableViewStylePlain frame:self.view.bounds];
tableView.contentOffset = CGPointMake(0.0f, innerViewSize.height);
Then, whenever you need to increase the size of the inner view, you change it's y origin and increase the contentOffset.y.

Programatically creating a UIView with multiple Labels

Xcode Interface Builder issue
Personally I do not like the way that interface builder works in Xcode. In this example I am trying to create a fairly complex view controller. On the viewDidLoad of the view controller I show a custom alert view (as such). It is not actually an alert view but more of a view that shows the user some information. I have a dimmed background view and a view on top of this. If I try to create this in interface builder it gets overly complicated as you cannot select the views in the background and move them etc without dropping subviews into the wrong views and so on...
Scenario
What I am trying to do is create a View which holds some labels and a button. The view controller has a difficulty property based on this it will have different text in the labels/amount of labels.
I.e. Easy -- 3 labels
Hard -- 4 labels
I create the dimmedView and alert(styled)View like this:
// Setup the dimmedView
UIView *dimmedView = [[UIView alloc] initWithFrame:self.view.frame];
dimmedView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.6];
// Setup the startingAlertView
UIView *startingAlertView = [[UIView alloc] init];
startingAlertView.backgroundColor = [UIColor whiteColor];
I then create the three/four labels based on some logic and add the necassary labels to the startingAlertView based on logic also.
The issue that is obvious is that at no point a frame for the view is set. This means that it is returning 0,0,0,0. What I would like to happen is the view to take the required height based on the labels added.
I am building for IOS7 and using Auto Layout. Should I be setting up constraints which would then adjust the relevant heights and locations in the view possibly?
I am building for IOS7 and using Auto Layout. Should I be setting up constraints which would then adjust the relevant heights and locations in the view possibly?
Yes. you don't use initWithFrame: under auto layout, or rather, you can, but the frame is ignored. Create your dimming view with a frame of CGRectZero, setting translatesAutoresizingMasksToConstraints to NO, add it to your main view and create constraints pinning it to all edges of the superview.
Then, add your alert view, again with a frame of zero and the translates... property set to NO. Create constraints to centre this view in your dimming view. This view will get its size from its subviews, since labels have an intrinsic size.
Add your labels as subviews of this view, with frame of zero and translates... set to NO. Depending on their content you may wish to set preferred max layout width or a width constraint.
Create constraints pinning your labels to the left and right edges of the superview, and lining your labels up in a vertical 'stack'. In each case you could add padding to give your alert a bit of a border.
This can look like a large amount of code, so you may want to read the articles I've written on visual format for auto layout and creating constraints in code, with the associated autolayout convenience category to make your life easier.
If you're going to the auto layout route, then you can add constraints that will keep the proper space between each label, and the proper space between the top and bottom of the view with the first and last labels. However, if you're not doing this in Interface Builder, you might as well skip using auto layout also, because it's fairly simple to just adjust the height of the view as you add labels.
You would start by setting the height of the view to the size of the top and bottom spaces that you want to have around the labels. Then each time you add a label, add to it the height of the label plus the height of the space you're putting between labels.
You could also wait until you've added all of the labels that you want, then set the height to the bottom label's y position plus its height plus the bottom space you want to have around the labels.
Yes, using autolayout you can get the bounds from the parent view.
Here is a quick example, notice that we are not using frame, and using CGRectZero for our UILabels, the positioning comes from updateConstraints instead. I am using Visual Format Language to layout the labels which I recommend if you are doing it programatically.
Here we are making the labels the width of the parent view and then just stacked on top of each other.
#import "View.h"
#implementation View{
UILabel *_label1;
UILabel *_label2;
UILabel *_label3;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
_label1 = [[UILabel alloc] initWithFrame:CGRectZero];
_label1.translatesAutoresizingMaskIntoConstraints = NO;
_label1.text = #"LABEL 1";
_label2 = [[UILabel alloc] initWithFrame:CGRectZero];
_label2.translatesAutoresizingMaskIntoConstraints = NO;
_label2.text = #"LABEL 2";
_label3 = [[UILabel alloc] initWithFrame:CGRectZero];
_label3.translatesAutoresizingMaskIntoConstraints = NO;
_label3.text = #"LABEL 3";
[self addSubview:_label1];
[self addSubview:_label2];
[self addSubview:_label3];
}
[self updateConstraintsIfNeeded];
return self;
}
-(void)updateConstraints
{
[super updateConstraints];
NSDictionary *_viewsDictionary = NSDictionaryOfVariableBindings(_label1,_label2,_label3);
// Set the contraintsto span the entire width of the super view
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[_label1]-|"
options:0
metrics:nil
views:_viewsDictionary];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[_label2]-|"
options:0
metrics:nil
views:_viewsDictionary];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[_label3]-|"
options:0
metrics:nil
views:_viewsDictionary];
[self addConstraints:constraints];
// Last setup the vertical contraints other wise they will end up in a random place
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[_label1]-[_label2]-[_label3]"
options:0
metrics:nil
views:_viewsDictionary];
[self addConstraints:constraints];
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
}
*/
#end

iOS AutoLayout not really working for views embedded into ScrollView

I'd like to embed views into each other (in the current case into a scrollview to allow displaying taller content than the screen's size) and use iOS 6.0's AutoLayout feature in order to avoid constant calculation of content size's.
I have the following ViewController's view, containing a UIScrollView:
I'd like to display my ChildVC's view in this scrollview:
Please notice that the label is multiline and contains a 'lot of' text, also auto-layout constraints are defined (escpecially with a greater-than-or-equal to the height property).
I create the childVC and add it to the main VC's view via the following code:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
childVC = [[ChildVC alloc] initWithNibName:#"ChildVC" bundle:nil];
self.myScrollView.translatesAutoresizingMaskIntoConstraints = NO;
childVC.view.translatesAutoresizingMaskIntoConstraints = NO;
[self.myScrollView addSubview:childVC.view];
NSDictionary *viewsDictionary = #{ #"subView" : childVC.view};
NSString* constr = [NSString stringWithFormat:#"|-0-[subView(%f)]-0-|", self.myScrollView.frame.size.width];
[self.myScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:constr options:0 metrics: 0 views:viewsDictionary]];
[self.myScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[subView]-(>=0)-|" options:0 metrics: 0 views:viewsDictionary]];
}
The constraints for the content-view (which I guess will be the childVC's view) are the only stuff set up in code, because I only want a vertical scrollbar, I want the child-view's content to (horizontally) shrink to be displayed in the scrollviewer.
And my output is this:
The UI is displayed, shrinked but my label does not resize vertically therefore it does not display the whole text :(
I tried to set up translatesAutoresizingMaskIntoConstraints to the label here and there without any success.
I'd appreciate any help because I'm struggling with this for days now :/
Personally, I think those constraints and the auto layout feature just don't always work exactly as we want them to. (Especially on different screen sizes).
So I normally add a few line of code in the viewDidLoad method to move certain objects around by a few pixels. You can also change sizes and so on.
Literally a simple movement like this does the trick without the user noticing any animations:
object_name.frame = CGRectOffset(object_name.frame, 0, -40.0f);

Using NSLayoutConstraints when adding a subview with simple animation

I'm trying to add a view (messageView) to my menu item (messageViewMenu) so that when the menu item is tapped, this new view is added just above it and they both slide down the screen together one behind the other - Evernote 5.1.2 has something similar.
I'm using an NSLayoutConstraint to attach the new view to what I assume is the top of the menu item. I then animate the menu item's existing vertical constraint to a header UIView (_headerMessageConstraint) to grow to size 300. I would expect the new view to be attached vertically with its trailing edge to the leading edge of the menu item and they slide down the screen together.
However, the new view slides down with the menu item and then continues on behind it until it seems the top edges are aligned.
Can anyone tell me where I'm going wrong?
Thanks in advance for your help
Steve
EDIT. Since I posted this I've learnt that that when I add the new view (MessageViewController's mvc.view) it's placed by default at 0,0. I don't want it at 0,0 though so it seems like I have to either set a frame for it at the location I want it to appear at - which seems wrong when using auto layout - or add this new view to a subview and animate the subviews height, perhaps... though I may be talking myself into a dark place here.
MessageViewController *mvc = [[MessageViewController alloc] init];
[mvc.view setTranslatesAutoresizingMaskIntoConstraints:NO];
UIView *messageView = mvc.view;
[self.view addSubview:messageView];
UIView *messageViewMenu = self.messageViewMenu;
NSDictionary* views = NSDictionaryOfVariableBindings(messageViewMenu, messageView);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[messageView]|"
options:0
metrics:nil
views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[messageView(200)]-[messageViewMenu]"
options:0
metrics:nil
views:views]];
[self.view layoutSubviews]; // UPDATE - THIS IS THE CODE THAT WAS MISSING - IT DISPLAYS THE SUBVIEWS BEFORE THE ANIMATION STARTS. THANKS #RDELMAR
[UIView animateWithDuration:0.9 animations:^{
_headerMessageConstraint.constant = 200;
[self.view layoutIfNeeded];
} completion:^(BOOL finished){}];
I'm not sure exactly what you're trying to do, but you need to add the line [self.view layoutSubviews] just above your animation block. The main view needs to layout your new view in its starting position, before you animate.
You also need to take out the dash in "V:[messageView(200)]-[messageViewMenu]" if you want the 2 views right on top of each other with no space.

Resources