I have a scrollview and a separate UIView where I placed a series of textFields and labels with constraints which fully occupies the top and bottom. I'm trying to adjust the UIView's height based on its subview constraints but it won't. What is happening is that the view keeps its height and force other textfields to collapse or shrink thus breaking the constraints.
Details
Each subview priority values :
compression = 750
hugging = 250
UIView priority values:
compression = 249
hugging = 749 Set to be lower than the rest.
Most of the textfields has aspect ratio constraint. This causes the field to adjust.
Each subview has vertical/top/bottom spacing between each other. The top and bottom elements has top and bottom constraints to the view as well.
What's on my code:
-(void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
/* I had to adjust the UIView's width to fill the entire self.view.*/
if(![contentView isDescendantOfView:detailsScrollView]){
CGRect r = contentView.frame;
r.size.width = self.view.frame.size.width;
contentView.frame = r;
[detailsScrollView addSubview:contentView];
}
}
Screenshots
The view
This is what currently happens. In this instance it forces the email field to shrink. If I place a height value on it, it does not shrink but the layout engine finds another element to break
Edit:
Solved
Maybe I just needed some break to freshen up a bit. I did tried using constraints before but got no luck. However thanks to the suggestion I went back setting the constraints instead of setting the frame on this one and got it finally working.
Solution:
-(void)viewDidLoad{
[detailsScrollView addSubview:contentView];
[contentView setTranslatesAutoresizingMaskIntoConstraints:NO];
[detailsScrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(contentView,detailsScrollView);
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[contentView]-0-|"
options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil
views:viewsDictionary];
NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[contentView]-0-|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:viewsDictionary];
NSArray *widthConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[contentView(==detailsScrollView)]-0-|" options:0 metrics:nil views:viewsDictionary];
}
When you use interface builder to deal with the UIScrollView and its child UIView. usually a top, bottom, left and equal width constraints are set between the UIScrollView and its child which is the contentView in your case.
Without those constraints the other option is to set the content size of the UIScrollView. which was the way of using the UIScrollView before introducing constraints.
So, 1. you should add those constraints programmatically.
By using the constraints, the views frame is no longer needed to resize the views.
So, 2. remove frame setting for your content view.
I am not so happy with the way you set the frame in the viewDidLayoutMethod. if I am going to do that here I would take the frame setting out of the if statement.
The code would be as follow with no if statement:
[detailsScrollView addSubview:contentView];
// then set the constraints here after adding the subview.
Put this code anywhere but not inside your viewDidLayoutSubviews method. it will be a bigger problem than setting the frame in there inside if statement.
Note: Originally, if you are going to set frame in the viewDidLayoutSubviews
method. you should do it for all cases. for example for the if case
and the else case. because, next time this method is going to be
called the views will respond to the constraint. and lose its frame.
Another observation: if you want the view to response to its subviews constraint why you need to set the frame for it? right?
After adding the constraint you may need to call the method constraintNeedsUpdate or another related method.
Related
This question already has answers here:
UITableView within UIScrollView using autolayout
(5 answers)
Closed 7 years ago.
I have this views hierarchy in a xib file:
UIView
UIScrollView
UIView
UIView
UITableView
UIButton
Let's call contentView the UIView that is the direct child of the UIScrollView. I've set its top, bottom, leading and trailing constraints to pin the scroll view. Then, since I'm populating the table view at runtime and I don't know its height beforehand, I set the scroll view's contentSize in code:
[self.scrollView setContentSize:CGSizeMake(self.contentView.frame.size.width, self.tableView.frame.size.height)];
But I don't make this work... what could I be missing?
AppsDev, check this video out it helped me a lot doing UIScrollView via storyboard
https://www.youtube.com/watch?v=UnQsFlMGDsI
Also never set the scrollView's contentSize as it has to be determined by scrollView on its own and that's why we have AutoLayout.
I believe we should have one tag for uiscrollview-autolayout
Don't set the content size manually. Instead,
Constrain your contentView's four edges to the edges of the scrollView.
Constrain your contentView's width to be equal to the scrollView's width. (This will prevent the content from being wider than the scrollView.)
Constrain the contentView's top and sides to the corresponding edges of the child view.
Constrain the contentView's sides and bottom to the sides and bottom of the tableView.
Now here's where it gets tricky: constrain the bottom of the child view to be equal to the top of the table view. However, unless you explicitly set a height constraint on the child view, you'll get a layout error that the height of the scrollView's contents will be ambiguous. To get around this, you can set the child's placeholder height to make Interface Builder happy, but then you'll also have to set its height somewhere at runtime.
Now you should be set. The scrollView can now calculate the full height and width of its contents by examining the constraint hierarchy, and you don't have to set its content height manually.
I finally managed to make this work by following the #Sana answer and also this post to be able to scroll the table view content.
Thanks u all for replying.
Just for example:
UIView *containerView = [[UIView alloc] init];
UIScrollView *scrollView = [[UIScrollView alloc] init];
[containerView addSubview:scrollView];
containerView.translatesAutoresizingMaskIntoConstraints = NO;
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(containerView, scrollView);
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[scrollView]|"
options:kNilOptions
metrics:nil
views:viewsDictionary]];
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[scrollView]|"
options:kNilOptions
metrics:nil
views:viewsDictionary]];
Use this code My code help you.
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self.scrollView setContentSize:CGSizeMake(self.contentView.frame.size.width, self.tableView.frame.size.height)];
}
This is the current setup I'm using. In my storyboard I have a ViewController with a UIScrollVIew that is position and sized with Autolayout.
The content of the scrollView is added programatically based on UIView built in interface builder. It has several labels, all of them using autolayout: leading and trailing to superview, and vertical spaces.
The scrollview gets resized accordingly to the screen size(iPhone / iPad), but the contentView's width is always the one specified in interface builder. I've tried setting contentview's frame width to match the scrollview frame width, and that changes it's frame but the labels width stay exactly the same. Also when I start to scroll it reverts my frame change. Any ideas how I can make the content's width match the scrollview's and update it's content accordingly?
I'm also adding constanints from the contentView to the scrollView programatically:
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[scrollContent]|" options:0 metrics: 0 views:viewsDictionary]];
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[scrollContent]|" options:0 metrics: 0 views:viewsDictionary]];
Setting the frame's width didn't work but apparently making a width constraint did the trick. Not sure why but maybe it helps other people as well.
I made a few UIScrollView's in different views, they all worked without Autolayout.
I turned Autolayout on, because it was better for my app.
But since then, there's a big problem with my UIScrollView's:
No one is scrolling, they don't work.
Here's my code for a UIScrollView:
.m:
-(viewDidLoad) {
scrollerHome.contentSize = CGSizeMake(320, 1000);
scrollerHome.scrollEnabled = YES;
[self.view addSubview:scrollerHome];
scrollerHome.showsHorizontalScrollIndicator = false;
scrollerHome.showsVerticalScrollIndicator = false;
[super viewDidLoad];
}
.h:
#interface ViewController : UIViewController{
IBOutlet UIScrollView *scrollerHome;
}
Do I have to add some code because I turned on Autolayout?
You should call [super viewDidLoad] before doing anything !
In autolayout, you do not set the contentSize manually. Autolayout works slightly differently with scrollviews, whereby the contentSize of the scroll view is dictated by the constraints of the scrollview's subviews.
If you're trying to force the contentSize to some large size (for example, you're implementing some infinite scroller), you can just add a subview of the appropriate size, e.g.:
UIView *containerView = [[UIView alloc] init];
containerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:containerView];
NSDictionary *views = NSDictionaryOfVariableBindings(containerView);
[self.scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[containerView]|" options:0 metrics:nil views:views]];
[self.scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[containerView(1000)]|" options:0 metrics:nil views:views]];
But if you were trying to set the contentSize in anticipation of adding subviews, you generally don't have to do anything, such as the above snippet. Just add your subviews, provide their constraints, and autolayout will adjust the scroll view's contentSize automatically.
As mentioned above, with autolayout, you can just add the subviews to your scrollview (with their constraints), and the contentSize will be calculated automatically for you.
There is a trick here, though. You sometimes you want to size a subview based upon the dimensions of the screen. But the usual technique of using the | symbols won't work. For example, for an imageview1 inside a scrollview, the usual #"H:|[imageview1]|" won't set the imageview1 to be the width of the screen, but rather it will define the scroll view's contentSize to match the width of imageview1, but it says nothing about what the width of that image view should be!
So, it's useful to capture a reference to the scroll view's superview. That way, you can use something like #"H:|[imageview1(==superview)]|", which not only says "make the scroll view's contentSize equal to the width of imageview1", but also "define the width of imageview1 to be equal to the width of the scroll view's superview."
Thus, for example, to add three images in a paging scroll view, you might do something like:
UIImageView *imageview1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"_DSC0004.jpg"]];
imageview1.contentMode = UIViewContentModeScaleAspectFit;
imageview1.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:imageview1];
UIImageView *imageview2 = ... // configured similar to imageview1
UIImageView *imageview3 = ... // configured similar to imageview1
UIView *superview = self.scrollView.superview;
NSDictionary *views = NSDictionaryOfVariableBindings(imageview1, imageview2, imageview3, superview);
// not only define the image view's relation with their immediate scroll view,
// but also explicitly set the size in relation to the superview, too!
[superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[imageview1(==superview)][imageview2(==superview)][imageview3(==superview)]|" options:0 metrics:nil views:views]];
[superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[imageview1(==superview)]|" options:0 metrics:nil views:views]];
[superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[imageview2(==superview)]|" options:0 metrics:nil views:views]];
[superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[imageview3(==superview)]|" options:0 metrics:nil views:views]];
self.scrollView.pagingEnabled = YES;
From the Apple iOS 6.0 release notes:
"In general, Auto Layout considers the top, left, bottom, and right edges of a view to be the visible edges. That is, if you pin a view to the left edge of its superview, you’re really pinning it to the minimum x-value of the superview’s bounds. Changing the bounds origin of the superview does not change the position of the view.
The UIScrollView class scrolls its content by changing the origin of its bounds. To make this work with Auto Layout, the top, left, bottom, and right edges within a scroll view now mean the edges of its content view."
You can find the full notes here and find the answer to your question in the section that I quoted from. They give code examples on how to use UIScrollView in a mixed Auto Layout environment.
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);
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 superview)
--- ViewA (full width, top of superview)
--- ViewB (full width, below ViewA)
--- Button (full width, below ViewB)
The ViewA and ViewB have initial heights of 200 points, but it can be expended vertically to an height of 400 points by clicking on it. ViewA and ViewB are expanded by updating their height constraint (from 200 to 400). Here is the corresponding snippet :
if(self.contentVisible) {
heightConstraint.constant -= ContentHeight;
// + additional View's internal constraints update to hide additional content
self.contentVisible = NO;
} else {
heightConstraint.constant += ContentHeight;
// + additional View's internal constraints update to show additional content
self.contentVisible = YES;
}
[self.view setNeedsUpdateConstraints];
[UIView animateWithDuration:.25f animations:^{
[self.view layoutIfNeeded];
}];
My problem is that if both views are expanded, I need to be able to scroll to see the whole content, and right now the scroll is not working. How can I manage to update the scroll view using constraints to reflect the changes of ViewA and ViewB heights ?
The only solution I can think of so far is to manually set the height of the ContainerView after the animation, which will be the sum of the heights of ViewA + ViewB + Button. But I believe there is a better solution?
Thanks
I use pure structure like the following
-view
-scrollView
-view A
-view B
-Button
Make sure Button(THE LAST view) has a constraint(vertical spacing from its bottom to superview, which is the scrollview), in this case, no matter what changes for your view A and view B would be, scrollView's height will be changed accordingly.
I reference to this great online book site.
Just read the "Creating a scroll view" section, you should have an idea.
I had the similar problem that I was creating a detail view and using Interface Builder with Auto layout is such a good fit for the task!
Good luck!
(Additional resources:
Stack overflow discussion about the auto layout for scroll view.
iOS 6 has a Release Notes talking about Auto Layout support for UIScrollView.
Free online iOS book explanation about scroll view. This actually helped me a lot!
Let's say we have a hierachy like this (Label1 is a subview of ContentView; ContentView is a subview of ScrollView, ScrollView is a subiview of the viewcontroller's view):
ViewController's View
ScrollView
ContentView
Label1
Label2
Label3
ScrollView is constrained with autolayout in the normal way to the viewcontroller's view.
ContentView is pinned top/left/right/bottom to scrollview. Meaning you have constraints that make the ContentView's top/bottom/leading/trailing edges constrained to be equal to the same edges on the ScrollView. Here is a key: these constraints are for the contentSize of the ScrollView, not its frame size as shown in the viewcontroller's view. So it's not telling the ContentView to be the same frame size as the displayed ScrollView frame, it's rather telling Scrollview that the ContentView is its content and so if contentview is larger than the ScrollView frame then you get scrolling, just like setting scrollView.contentSize larger than scrollView.frame makes the content scrollable.
Here is another key: now you have to have enough constraints between ContentView, Label1-3, and anything else besides the Scrollview for the ContentView to be able to figure out it's width and height from those constraints.
So for example if you want a vertically scrolling set of labels, you set a constraint to make the ContentView width equal to the ViewController View's width, that takes care of the width. To take care of the height, pin Label1 top to ContentView top, Label2 top to Label1 bottom, Label3 top to Label2 bottom, and finally (and importantly) pin Label3's bottom to ContentView's bottom. Now it has enough information to calculate the ContentView's height.
I hope this gives someone a clue, as I read through the above posts and still couldn't figure out how to make the ContentView's width and height constraints properly. What I was missing was pinning the Label3's bottom to the ContentView's bottom, otherwise how could ContentView know how tall it is (as Label3 would just then be floating, and there would be no constraint to tell ContentView where it's bottom y position is).
This is an example of how I have laid out a pure autolayout UIScrollView with a container view. I've commented to make it clearer:
container is a standard UIView and body is a UITextView
- (void)viewDidLoad
{
[super viewDidLoad];
//add scrollview
[self.view addSubview:self.scrollView];
//add container view
[self.scrollView addSubview:self.container];
//body as subview of container (body size is undetermined)
[self.container addSubview:self.body];
NSDictionary *views = #{#"scrollView" : self.scrollView, #"container" : self.container, #"body" : self.body};
NSDictionary *metrics = #{#"margin" : #(100)};
//constrain scrollview to superview, pin all edges
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[scrollView]|" options:kNilOptions metrics:metrics views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[scrollView]|" options:kNilOptions metrics:metrics views:views]];
//pin all edges of the container view to the scrollview (i've given it a horizonal margin as well for my purposes)
[self.scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[container]|" options:kNilOptions metrics:metrics views:views]];
[self.scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-margin-[container]-margin-|" options:kNilOptions metrics:metrics views:views]];
//the container view must have a defined width OR height, here i am constraining it to the frame size of the scrollview, not its bounds
//the calculation for constant is so that it's the width of the scrollview minus the margin * 2
[self.scrollView addConstraint:[NSLayoutConstraint constraintWithItem:self.container attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.scrollView attribute:NSLayoutAttributeWidth multiplier:1.0f constant:-([metrics[#"margin"] floatValue] * 2)]];
//now as the body grows vertically it will force the container to grow because it's trailing edge is pinned to the container's bottom edge
//it won't grow the width because the container's width is constrained to the scrollview's frame width
[self.container addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[body]|" options:kNilOptions metrics:metrics views:views]];
[self.container addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[body]|" options:kNilOptions metrics:metrics views:views]];
}
In my example 'body' is a UITextView, but it could be anything else. If you happen to be using a UITextView as well note that in order for it to grow vertically it must have a height constraint that gets set in viewDidLayoutSubviews. So add the following constraint in viewDidLoad and keep a reference to it:
self.bodyHeightConstraint = [NSLayoutConstraint constraintWithItem:self.body attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:nil multiplier:1.0f constant:100.0f];
[self.container addConstraint:self.bodyHeightConstraint];
Then in viewDidLayoutSubviews calculate the height and update the constraint's constant:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self.bodyHeightConstraint setConstant:[self.body sizeThatFits:CGSizeMake(self.container.width, CGFLOAT_MAX)].height];
[self.view layoutIfNeeded];
}
The second layout pass is needed to resize the UITextView.
Use this code. ScrollView setContentSize should be called async in main thread.
Swift:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
DispatchQueue.main.async {
var contentRect = CGRect.zero
for view in self.scrollView.subviews {
contentRect = contentRect.union(view.frame)
}
self.scrollView.contentSize = contentRect.size
}
}
Objective C:
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
dispatch_async(dispatch_get_main_queue(), ^ {
CGRect contentRect = CGRectZero;
for(UIView *view in scrollView.subviews)
contentRect = CGRectUnion(contentRect,view.frame);
scrollView.contentSize = contentRect.size;
});
}
At every moment the scroll view should know its content size. The content size is inferred from the scrollview's subviews. It is very handy to map controller properties to the constraints in the xib file describing heights of the subviews. Then in the code (an animation block) you can just change constants of these constraint properties. If you need to change the entire constraint, keep a reference to it, so that you can update it later in the parent container.
My variant for scroll view with !Dynamic! height:
1) Add scroll view to your UIView. Pin all (top, bottom, lead, trail) constraints.
2) Add UIView to Scroll View. Pin all (top, bottom, lead, trail) constraints. It will be your Content view. You can also rename it.
3) Control drag from Content view to Scroll view - Equal width
4) Add content to your UIView. Set needed constraints. And! At the lower item add bottom constraint NOT Greater or equal (>=)(Like most people talks) BUT Equal! Set it to 20 for example.
In my situation I have UIImageView in content. I have connected it's height to code. And if I change it to like 1000, scroll is visible. And all works.
Works like a charm for me. Any questions - welcome to comments.