Auto layout of a Child Container that embeds a UIViewController - ios

My main scene shows a child UIViewController, by putting a Child Container in its View, and embedding a child UIViewController in that Child Container. I do that by ctrl-dragging from the Child Container to the child UIViewController in the storyboard, and choosing the "embed" segue.
That lets me encapsulate and reuse the child UIViewController elsewhere. I think that is fairly standard. But I can't get it to auto layout properly.
I've made a simple test case to demonstrate this:
https://github.com/murraycu/ios-example-autolayout-of-child-container-views
It has two UIViewControllers that are embedded in a Tab Controller so you can switch between them.
The "Simple" tab shows the SimpleViewController, which shows an image and a label, seen here in the storyboard:
Neither the UIImage or the UILabel has a height constraint specified, though they have an Equal Width (equal to the parent view) constraint to keep the widths simple.
That UILabel is clearly not very high but the UIImage and UILabel have slightly different Content Hugging priorities, making their heights not ambiguous according to Auto Layout. So thanks to auto layout, when I set its text at runtime to some text that would require more space, it takes up more space, taking space away from the UIImage above it. That's good - it is the behaviour that I want, as seen in the emulator:
Now to the problem: The "With Child Container" tab shows the WithChildContainerViewController, which shows the same image but shows my ChildViewController (embedded in a Child Container) instead of the UILabel. And that embedded ChildViewController shows the UILabel. Here it is in the storyboard:
However, the auto layout system now doesn't seem to know how much space the Child Container needs to show all the text in the label in my ChildViewController. As in the "Simple" tab, neither the UIImage or the Child Container has a height constraint specified. Now XCode complains that "Height is ambiguous for "container view". And it looks like this in the simulator:
That's improved if I add a constraint to the Child Container, constraining its bottom to the bottom of the parent view, as suggested by #iphonic: https://github.com/murraycu/ios-example-autolayout-of-child-container-views/commit/1d295fe0a6c4502764f8725d3b99adf8dab6b9ae but the height is still wrong:
How can I let the auto layout system know what to do? I've thought about implementing UIView's intrinsicContentSize, but UIViewController isn't a UIView.

Suggestion is, do it programatically rather than via IB. See below
_childController=[self.storyboard instantiateViewControllerWithIdentifier:#"ChildController"];
[self addChildViewController:_childController];
[_container addSubview:_childController.view];
[_childController didMoveToParentViewController:self];
_childController.view.frame=_container.bounds;
//add constraints
UIView *subView=_childController.view;
UIView *parent=_container;
subView.translatesAutoresizingMaskIntoConstraints=NO;
NSLayoutConstraint *width =[NSLayoutConstraint
constraintWithItem:subView
attribute:NSLayoutAttributeWidth
relatedBy:0
toItem:parent
attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0];
NSLayoutConstraint *height =[NSLayoutConstraint
constraintWithItem:subView
attribute:NSLayoutAttributeHeight
relatedBy:0
toItem:parent
attribute:NSLayoutAttributeHeight
multiplier:1.0
constant:0];
NSLayoutConstraint *top = [NSLayoutConstraint
constraintWithItem:subView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:parent
attribute:NSLayoutAttributeTop
multiplier:1.0f
constant:0.f];
NSLayoutConstraint *leading = [NSLayoutConstraint
constraintWithItem:subView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:parent
attribute:NSLayoutAttributeLeading
multiplier:1.0f
constant:0.f];
[parent addConstraint:width];
[parent addConstraint:height];
[parent addConstraint:top];
[parent addConstraint:leading];
Hope it helps.
Cheers.

#iphonic's code but using Visual Format Language
_childController = [self.storyboard instantiateViewControllerWithIdentifier: #"ChildController"];
[self addChildViewController: _childController];
[_container addSubview: _childController.view];
[_childController didMoveToParentViewController: self];
//add constraints
NSDictionary *views = #{#"subView": _childController.view,
#"parent": _container};
[parent addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat: #"V:|[subView(==parent)]"
options: 0
metrics: nil
views: views]];
[parent addConstraints: [NSLayoutConstraint
constraintsWithVisualFormat:#"H:|[subView(==parent)]"
options: 0
metrics: nil
views: views]];

Related

Embed a UIView in another UIView with cascading Autolayout

My goal:
I'm trying to find a general recipe how to add views that I created in Interface Builder to a parent view that I create in Interface Builder as well, while letting their layout constraints determine the size of their respective parent views.
The key idea is to get cascading views with a variable size. A view should first determine its own size by resolving its layout constraints. Once it's done it has a fixed size from its superview's perspective so now the superview can determine its size by resolving its own layout constraints and so on.
My approach:
I create a new XIB file in Xcode and open it in Interface Builder. Next, I drag another UIView from the Object library to the XIB's view in order to add it as a subview. Using layout constraints I pin all four sides of the subview to its superview's edges:
Now I create another XIB file which is supposed to be the subview. I drag a UILabel and a UIButton to its view. I give the button a fixed height constraint and add more constraints to pin the label and the button to the view's edges:
Now comes the tricky part that I haven't been able to solve yet:
I couldn't find any way to add the view of this second XIB file to the subView of the first XIB using only Interface Builder (is this even possible?) so I tried it by writing some code. I created a UIView custom class and set the class value in Interface Builder accordingly. In the custom class I added this method:
- (void)awakeFromNib {
UIView *view = [[[NSBundle mainBundle] loadNibNamed:#"DynamicSubView" owner:self options:nil] objectAtIndex:0];
[self.subView addSubview:view];
view.translatesAutoresizingMaskIntoConstraints = NO;
[self addConstraintsForView:view];
}
- (void)addConstraintsForView:(UIView *)view {
[self addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.subView attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.subView attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.subView attribute:NSLayoutAttributeTop multiplier:1.0 constant:0]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.subView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0]];
}
The method addLayoutConstraintsForView: adds constraints to the view from the second XIB that pins all its four edges to the subView's edges. Now the subView should resize dynamically with the view it contains. However, it doesn't seem to work. Any idea or a good recipe how to deal with this?
Remark: The top most view here is intended to be a UITableViewCell. I want it to layout itself and pass its height to its tableView using the method introduced here.

Autolayout and UIScrollview and Child View Controller issues

In my app I have an embedded child view controller into which at runtime I have to add one or more "Editor UIView"s based on data coming in from the server. I could model each "Editor" as a complete UIViewController in a xib file and add it at run time. The parent of the "Editor" has some items of its own (like titles and a couple of buttons) so I've tried to build a layered approach using a single child UIViewController and loading the "Editors" into its UIScrollview on the fly. However I can't get Autolayout to cooperate. This diagram shows the basic arrangement.
The problems start with just having the "Content View" embedded in the UIScrollview. I can get the content view to work OK with the scrollview. When I add the editor view and add runtime constraints to the content view, I get multiple auto layout complaints plus the scroll view content size is 0,0 so I am clearly not getting what I need.
Any ideas on how to approach this? I could always simply duplicate the child controller's extra items for each editor but it would be nice to have this work in this layered fashion.
Note I did this first with a UITableViewController where each editor is a UITableViewCell and that auto layout liked but I wanted to see if I could do it without the table. Maybe I will go back to that.
Added these at runtime:
_editor = [self editorForDataType];
[_contentView addSubview:_editor];
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:_editor
attribute:NSLayoutAttributeLeading
relatedBy:0
toItem:self.contentView
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:0];
[self.contentView addConstraint:leftConstraint];
NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:_editor
attribute:NSLayoutAttributeTrailing
relatedBy:0
toItem:self.contentView
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:0];
[self.contentView addConstraint:rightConstraint];
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:_editor
attribute:NSLayoutAttributeLeading
relatedBy:0
toItem:self.contentView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0];
[self.contentView addConstraint:topConstraint];
NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:_editor
attribute:NSLayoutAttributeTrailing
relatedBy:0
toItem:self.contentView
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0];
[self.contentView addConstraint:bottomConstraint];
In the end the key to what I needed was the follow set of constraints, plus adding the constraints for the editor view in viewDidLayoutSubviews and saving a property for the content view Height.
In viewDidLoad I set the height property to the height of the editor view. Now it works great.
Your last two constraints are incorrect. You have leading to top and trailing to bottom, those should be top to top and bottom to bottom. However, those constraints probably aren't what you want. If it's the first editor view, you probably want the top constraint to the content view and a height constraint. For any subsequent ones, you should pin the top to the editor view above it, rather than the content view.

iOS: Autolayout differences with appliying constraints to subview on iOS 7 and iOS 8

I have a view controller set up in a storyboard. The view controller's view contains a subview(UITableView) with pinned edges to the 4 sides of its parent, essentially making the view fill its parent.
I am adding the view controller's main view as a subview of another view controller's view like this:
UIView *overlayView = firstViewController.view;
[overlayView setTranslatesAutoresizingMaskIntoConstraints:NO];
UIView *sourceView = secondViewController.view;
[sourceView addSubview:overlayView];
NSLayoutConstraint *constraint;
constraint = [NSLayoutConstraint constraintWithItem:overlayView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:sourceView attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0];
[sourceView addConstraint:constraint];
constraint = [NSLayoutConstraint constraintWithItem:sourceView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:overlayView attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:100];
[sourceView addConstraint:constraint];
constraint = [NSLayoutConstraint constraintWithItem:overlayView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:sourceView attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];
[sourceView addConstraint:constraint];
constraint = [NSLayoutConstraint constraintWithItem:overlayView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:sourceView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0];
[sourceView addConstraint:constraint];
[sourceView layoutIfNeeded];
I want to have a gap from the right edge of the view to its parent equal to 100 pixels.
Now weirdly enough this works as expected on iOS 8, but fails to do so on iOS 7 and the view is displayed full-screen ignoring the 100 constant set on the trailing constraint. Also, if the view controller's view that is being added has no child views - then it is working as expected. Is setTranslatesAutoresizingMaskIntoConstraints: being applied to all the subviews of a view in iOS 7 ? What might be the reason for this ?
EDIT:
The issue lies somewhere in the way subviews are being managed by the OS.
When the Container View is set as a outlet to the view property of the view controller, things don't work. If however, I set the Table View as an outlet to the view property, the it works. Something weird happens if there is a child view with pinned edges to its superview and then I am adding other constraints to the superview. I dont understand why it works fine on iOS 8 though...
EDIT 2
The problem seems to happen only with the trailing constraint. If I want to modify the constants of any of the other constraints there are no issues ?!
Can you try interchanging sourceView with overlayView in your constraint?
`constraint = [NSLayoutConstraint constraintWithItem:sourceView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:overlayView attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:100];`
This essentially means that sourceView.trailing - 100 = overlayView.trailing; which is not what you would want.
Edit 1:
Is setTranslatesAutoresizingMaskIntoConstraints: being applied to all the subviews of a view in iOS 7?
NO. translatesAutoresizingMaskIntoConstraints is set only for the views you explicitly call setTranslatesAutoresizingMaskIntoConstraints for.

Center UIView on screen using Auto Layout programmatically when the view is located at the top of the view hierarchy

I have implemented a popup UIView which I add to the topmost window via [[[[UIApplication sharedApplication] windows] lastObject] addSubview:popupView], so it will appear on top of everything even the keyboard. I need to ensure this popup view that I programmatically created will always remain centered on screen. I was attempting to add auto layout constraints, but it doesn't like the fact I'm trying to align with the topmost window. Could you let me know how I could accomplish this? Thank you.
This is what I have implemented, which will generate a (nicely detailed) error that states 'The view hierarchy is not prepared for the constraint ... the constraint's items must be descendants of that view':
[popupView setTranslatesAutoresizingMaskIntoConstraints:NO];
NSLayoutConstraint *cn = nil;
cn = [NSLayoutConstraint constraintWithItem:popupView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:[[[UIApplication sharedApplication] windows] lastObject]
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0];
[popupView addConstraint:cn];
cn = [NSLayoutConstraint constraintWithItem:popupView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:[[[UIApplication sharedApplication] windows] lastObject]
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0];
[popupView addConstraint:cn];
cn = [NSLayoutConstraint constraintWithItem:popupView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1
constant:blockSize.height];
[popupView addConstraint:cn];
cn = [NSLayoutConstraint constraintWithItem:popupView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1
constant:blockSize.width];
[popupView addConstraint: cn];
Don't go sticking your view into a window that wasn't set up specifically to hold your views. You don't know what the real owner of that window might do, now or in a future version of iOS.
Create your own window and set its windowLevel high enough to be above the keyboard. Give the new window its own root view controller (so it will handle different orientations properly) and center your popup view inside that root view controller's view.
You will find lots of useful information in the answers to this question.
It's fine to add the height and width constraints to popupView, but since popupView is a subview of the window, the centering constraints should be added to the window, not the popupView (and you need to add the popupView as a subview first, before you add the constraints).

iOS Auto Layout issue

I'm trying to make constraints programmatically. I have textField and button created in IB. Here is the code:
UIView *superview = self.view;
self.button.translatesAutoresizingMaskIntoConstraints = NO;
self.textField.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self.textField
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeCenterY
multiplier:1.0f
constant:0];
[superview addConstraint:constraint];
constraint = [NSLayoutConstraint constraintWithItem:self.textField
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeCenterX
multiplier:1.0f
constant:0];
[superview addConstraint:constraint];
constraint =[NSLayoutConstraint constraintWithItem:self.button
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.textField
attribute:NSLayoutAttributeLeading
multiplier:1
constant:-10];
[superview addConstraint:constraint];
constraint =[NSLayoutConstraint constraintWithItem:self.button
attribute:NSLayoutAttributeBaseline
relatedBy:NSLayoutRelationEqual
toItem:self.textField
attribute:NSLayoutAttributeBaseline
multiplier:1
constant:0];
[superview addConstraint:constraint];
And after running I have this issue:
Constraints also have no effect on views.
What am I doing wrong?
First in the storyboard editor select the view controller you want to change then, in the constraint editor, select "ADD missing constraints in view controller"
Select an elment that you'd like to modify at runtime by remomiving and adding new constraint, in the object inspector panel select each constraints and edit it by checking the box "placeholder: remove at build time".
This is a way to say to storyboard editor that you are fine with the constraint ant it should not apply or complain about an insufficient constraints situation.
Constraints flagged are removed at runtime, so you should provide you own constraint and is better if you do in the right place. Ovverride -updateViewConstraints in the view controller and remember to call super and add you new constraints.
You have three auto-generated constraints "IB auto generated at build time for view with fixed frame". These conflict with the ones you make yourself. Probably, you have set fixed size properties on the text field and button in Interface Builder. Possibly, you have a fixed width on your button, and that conflicts with the trailing constraint.
Combining IB with code constraints can be tricky. I have found it is often easier to design things only in IB or only in code to avoid conflicts like these, although combining the two is perfectly viable.
For a good explanation on what NSIBPrototypingLayoutConstraint is, have a look at this SO answer:
Trouble with AutoLayout on UITableViewCell

Resources