Challenge time!
Imagine we have 2 content views:
UIView with dynamically height content (expandable UITextView) = RED
UIView as a footer = BLUE
This content is inside a UIScrollView = GEEN
How should I structure and handle the constraints with auto-layout to archive all the following cases?
I am thinking next basic structure to start with:
- UIScrollView (with always bounce vertically)
- UIView - Container
- UIView - DynamicHeightContent
- UIView - Sticky Footer
Keyboard handling should be done by code watching notifications UIKeyboardWillShowNotification and UIKeyboardWillHideNotification. We can chose to set the keyboard's end frame height to Container UIView bottom pin constraint or to the UIScrollView bottom contentInset.
Now, the tricky part is the sticky footer.
How we make sure the sticky footer UIView stays at the bottom if there is more screen available than the whole Container View?
How do we know the available screen space when the keyboard is shown/hidden? we'll surely need it.
Is is it right this structure I purpose?
Thank you.
When the text content of the UITextView is relatively short, the content view's subviews (i.e., the text view and footer) will not be able to dictate the size of their content view through constraints. That's because when the text content is short, the content view's size will need to be determined by the scroll view's size.
Update: The latter paragraph is untrue. You could install a fixed-height constraint either on the content view itself or somewhere in the content view's view hierarchy. The fixed-height constraint's constant could be set in code to reflect the height of the scroll view. The latter paragraph also reflects a fallacy in thinking. In a pure Auto Layout approach, the content view's subviews don't need to dictate the scroll view's contentSize; instead, it's the content view itself that ultimately must dictate the contentSize.
Regardless, I decided to go with Apple's so-called "mixed approach" for using Auto Layout with UIScrollView (see Apple's Technical Note: https://developer.apple.com/library/ios/technotes/tn2154/_index.html)
Some iOS technical writers, like Erica Sadun, prefer using the mixed approach in pretty much all situations ("iOS Auto Layout Demystified", 2nd Ed.).
In the mixed approach, the content view's frame and the scroll view's content size are explicitly set in code.
Here's the GitHub repo I created for this challenge: https://github.com/bilobatum/StickyFooterAutoLayoutChallenge. It's a working solution complete with animation of layout changes. It works on different sized devices. For simplicity, I disabled rotation to landscape.
For those who don't want to download and run the GitHub project, I have included some highlights below (for the complete implementation, you'll have to look at the GitHub project):
The content view is orange, the text view is gray, and the sticky footer is blue. The text is visible behind the status bar while scrolling. I don't actually like that, but it's fine for a demo.
The only view instantiated in storyboard is the scroll view, which is full-screen (i.e., underlaps status bar).
For testing purposes, I attached a double tap gesture recognizer to the blue footer for the purpose of dismissing the keyboard.
- (void)viewDidLoad
{
[super viewDidLoad];
self.scrollView.alwaysBounceVertical = YES;
[self.scrollView addSubview:self.contentView];
[self.contentView addSubview:self.textView];
[self.contentView addSubview:self.stickyFooterView];
[self configureConstraintsForContentViewSubviews];
// Apple's mixed (a.k.a. hybrid) approach to laying out a scroll view with Auto Layout: explicitly set content view's frame and scroll view's contentSize (see Apple's Technical Note TN2154: https://developer.apple.com/library/ios/technotes/tn2154/_index.html)
CGFloat textViewHeight = [self calculateHeightForTextViewWithString:self.textView.text];
CGFloat contentViewHeight = [self calculateHeightForContentViewWithTextViewHeight:textViewHeight];
// scroll view is fullscreen in storyboard; i.e., it's final on-screen geometries will be the same as the view controller's main view; unfortunately, the scroll view's final on-screen geometries are not available in viewDidLoad
CGSize scrollViewSize = self.view.bounds.size;
if (contentViewHeight < scrollViewSize.height) {
self.contentView.frame = CGRectMake(0, 0, scrollViewSize.width, scrollViewSize.height);
} else {
self.contentView.frame = CGRectMake(0, 0, scrollViewSize.width, contentViewHeight);
}
self.scrollView.contentSize = self.contentView.bounds.size;
}
- (void)configureConstraintsForContentViewSubviews
{
assert(_textView && _stickyFooterView); // for debugging
// note: there is no constraint between the subviews along the vertical axis; the amount of vertical space between the subviews is determined by the content view's height
NSString *format = #"H:|-(space)-[textView]-(space)-|";
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:format options:0 metrics:#{#"space": #(SIDE_MARGIN)} views:#{#"textView": _textView}]];
format = #"H:|-(space)-[footer]-(space)-|";
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:format options:0 metrics:#{#"space": #(SIDE_MARGIN)} views:#{#"footer": _stickyFooterView}]];
format = #"V:|-(space)-[textView]";
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:format options:0 metrics:#{#"space": #(TOP_MARGIN)} views:#{#"textView": _textView}]];
format = #"V:[footer(height)]-(space)-|";
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:format options:0 metrics:#{#"space": #(BOTTOM_MARGIN), #"height": #(FOOTER_HEIGHT)} views:#{#"footer": _stickyFooterView}]];
// a UITextView does not have an intrinsic content size; will need to install an explicit height constraint based on the size of the text; when the text is modified, this height constraint's constant will need to be updated
CGFloat textViewHeight = [self calculateHeightForTextViewWithString:self.textView.text];
self.textViewHeightConstraint = [NSLayoutConstraint constraintWithItem:self.textView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:1.0f constant:textViewHeight];
[self.textView addConstraint:self.textViewHeightConstraint];
}
- (void)keyboardUp:(NSNotification *)notification
{
// when the keyboard appears, extraneous vertical space between the subviews is eliminated–if necessary; i.e., vertical space between the subviews is reduced to the minimum if this space is not already at the minimum
NSDictionary *info = [notification userInfo];
CGRect keyboardRect = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
keyboardRect = [self.view convertRect:keyboardRect fromView:nil];
double duration = [[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
CGFloat contentViewHeight = [self calculateHeightForContentViewWithTextViewHeight:self.textView.bounds.size.height];
CGSize scrollViewSize = self.scrollView.bounds.size;
[UIView animateWithDuration:duration animations:^{
self.contentView.frame = CGRectMake(0, 0, scrollViewSize.width, contentViewHeight);
self.scrollView.contentSize = self.contentView.bounds.size;
UIEdgeInsets insets = UIEdgeInsetsMake(0, 0, keyboardRect.size.height, 0);
self.scrollView.contentInset = insets;
self.scrollView.scrollIndicatorInsets = insets;
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
[self scrollToCaret];
}];
}
Although the Auto Layout component of this demo app took some time, I spent almost as much time on scrolling issues related to a UITextView being nested inside of a UIScrollView.
Instead of using a UIScrollView you would very likely be better off with a UITableView. It also might be better to not using auto-layout. At least, I've found it better to not use it for these sorts of manipulations.
Look into the following:
UITextView textViewDidChange
Change the size of the text view using sizeThatFits (limiting width and using FLT_MAX for height). Change the frame, not the contentSize.
Call UITableView beginUpdates/endUpdates to update the table view
Scroll to the cursor
UIKeyboardWillShowNotification notification
On NSNotification that comes through, you can call userInfo (a Dictionary), and the key UIKeyboardFrameBeginUserInfoKey. Reduce the frame of the table view based on the height of the size of the keyboard.
Scroll to cursor again (since the layouts will have all changed)
UIKeyboardWillHideNotification notification
The same as the show notification, just opposite (increasing the table view height)
To have the footer view stick to the bottom, you could add an intermediate cell to the table view, and have it change size depending on the size of the text and whether the keyboard is visible.
The above will definitely require some extra manipulation on your part - I don't fully understand all of your cases, but it should definitely get you started.
If I understand whole task, my solution is put "red" and "blue" views to one container view, and in the moment when you know size of dynamic content (red) you can calculate size of container and set scrollView content size.
Later, on keyboard events you can adjust white space between content and footer views
Related
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.
I'm trying to make UICollectionView work like UITableView (I want the extra flexibility of collection view instead of just going with table view, both for some current and possible future feature extensions); having a fixed width (screen width) and dynamic cell height (just like iOS 8's new table view feature). I've been struggling a lot to get it working. First, I've enabled automatic sizing by setting an estimated size on layout on my collection view subclass:
UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout*)self.collectionViewLayout;
layout.estimatedItemSize = CGSizeMake(SCREEN_WIDTH, 100);
Then I've set up my constraints of my custom cell in Interface Builder to let it grow according to my text view's content.
I've also set up a width constraint equal to screen width programatically, otherwise my cells were having a default width of 50pt. Inside my custom cell's awakeFromNib:
[self addConstraint: [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:SCREEN_WIDTH]];
It seems to work for width. When I run my app, it breaks height constraints as my own constraints conflict with UIView-Encapsulated-Layout-Height which is set to 100 (this actually means that auto-height is not really working well).
I lower the priority from my vertical constraints, especially the one with textview (which grows depending on its content) from required (1000) to some lower number (900). I'm not getting broken constraints anymore, but that's because we don't need to break constraints anymore as UIView-Encapsulated-Layout-Height takes precedence. Result is exactly the same. Here is what I'm getting:
There is no problem with the GIF above. It really does jump at one point exactly as seen on the GIF.
How can I prevent this behavior and make my collection view cells grow in height dynamically (of course, without creating a dummy offscreen cell to calculate view sizes)?
I don't need to support iOS 7, so iOS 8-and-above-only solutions are welcome.
UPDATE: If I implement preferredLayoutAttributesFittingAttributes: method as seen on the answer to UICollectionView Self Sizing Cells with Auto Layout:
-(UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes{
UICollectionViewLayoutAttributes *attrs = [layoutAttributes copy];
CGRect frame = attrs.frame;
self.frame = frame;
[self setNeedsLayout];
[self layoutIfNeeded];
float desiredHeight = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
frame.size.height = desiredHeight;
attrs.frame = frame;
return attrs;
}
Then it's always returning the NIB's original view size that was in the Interface Builder even though I've set:
self.translatesAutoresizingMaskIntoConstraints = NO;
self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
I have no idea why it's returning an incorrect value.
After spending hours and days, I've decided to go back to table view. While collection view offers great flexibility and is definitely the future, it's just not there yet.
I have a UIScrollView with a 6 textfields in it and a button inside of it. There is not enough content in the scrollView to make it scroll.
But when the keyboard shows, I would like the scrollview to scroll so the user doesn't have to dismiss the keyboard in order to select another textfield that is hidden by the keyboard.
I am using iOS7 and have autolayout enabled.
Any suggestions?
I am using storyboards and the only code I have is the following.
reg.h file
interface registerViewController : UIViewController <UITextFieldDelegate, UIScrollViewDelegate>
In order to make a scrollview scrollable, the content size must be larger than the scrollview's frame so the scrollview has something to scroll to. Use setContentSize to adjust the content size:
[scrollview setContentSize:CGSizeMake(width, height)];
In this case, you should adjust the size to view.frame.width, view.frame.height + keyboard_height, then adjust the content offset once the keyboard appears:
[scrollview setContentOffset:CGPointMake(0, 0 - keyboard_height)];
If for some screwy, autolayout-related reason this still doesn't make the view scrollable, implement this setContentSize function in viewDidLayoutSubviews in order to override the autolayout:
- (void)viewDidLayoutSubviews {
[scrollview setContentSize:CGSizeMake(width, height)];
}
EDIT: To reset the scrollview after dismissing the keyboard, reset the scrollview content size to the scrollview's frame and the offset to zero:
[scrollview setContentSize:CGSizeMake(scrollview.frame.size.width, scrollview.frame.size.height)];
[scrollview setContentOffset:CGPointZero];
P.S. To animate the content offset, use:
[scrollview setContentOffset:offsetSize animated:YES];
There is a contentInset property of UIScrollViews, you can set the contentInset to make additional space at the bottom to allow for scrolling without changing contentSize.
UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, 100, 0.0);
scrollView.contentInset = contentInsets;
above code adds 100 points inset at the bottom.
By the way, there is an official document about this matter. It explains everything you should do. You can find it here. You can find what you are looking for under the section 'Moving Content That Is Located Under the Keyboard'
Try
Create Scroll view
Add View to the scroll view (In my case i added view as mainView).
Set ScrollView autoresizing.
Set MainView autoresizing.
To set the Scroll content Size equal to the view created add below line
Add the below line
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.scrollView.contentSize = self.mainView.frame.size;
}
I'm using Auto Layout in my iOS 7 project with the following view hierarchy
Main View
-Container View
---Button
---Button
---ImageView
-Banner View (iAd Banner View)
The Main View and Container View are full width and height of screen. I have Horizontal and Vertical Space Constraints on the Container View sticking to the main view (screen's height and width). And also the subviews of Container View are constrained to the button of the view with a 20px space.
My issue occurs when the Banner View is finally filled and placed at the bottom of the screen, which then I have the Container View subtract the Banner View's Height from its frame height to allow space for the Banner View to show. (code used below) The ideal outcome is the Container View to subtract the height and its subviews constraint update based on this new height ,but what end up happening is the iAD Banner View just overlays the view as shown in the picture.
Code for BannerViewDidLoadAd:
- (void)bannerViewDidLoadAd:(ADBannerView *)banner
{
CGRect contentFrame = self.containerView.bounds;
CGRect bannerFrame = self.bannerView.bounds;
if (self.bannerView.bannerLoaded) {
contentFrame.size.height = self.containerView.frame.size.height - self.bannerView.frame.size.height;
bannerFrame.origin.y = contentFrame.size.height;;
} else {
bannerFrame.origin.y = contentFrame.size.height;
}
[UIView animateWithDuration:animated ? 0.25 : 0.0 animations:^{
[self.containerView setFrame:contentFrame];
[self.containerView layoutIfNeeded];
self.bannerView.frame = bannerFrame;
self.bannerView.hidden = NO;
}];
[self.containerView updateConstraints];
}
Image of iAd overlaying Container View and it's SubViews
After you create the banner view in code (and add it as a subview of main view), you should add a 0 length spacing constraint between the bottom of the container view, and the top of the banner view (the banner view would need constraints to the two sides of the main view and a height constraint as well). The container view should have 0 length constraints to all four edges of the main view. You should make an IBOutlet to that bottom constraint, and animate that constraint's constant value by an amount equal to the height of the banner view (so it will shrink, and the banner view will move up with it due to its 0 length vertical spacing constraint). So, if the outlet to the bottom constraint was called bottomCon, and the height of the banner view was 100 points, you would animate like this:
[UIView animateWithDuration:animated ? 0.25 : 0.0 animations:^{
self.bottomCon.constant = 100;
[self.mainView layoutIfNeeded];
}];
There's no need to hide and unhide the view, since you will initially place it off the bottom of the screen anyway. Also make sure that you call [bannerView setTranslatesAutoresizingMaskIntoConstraints:NO] right after you create the banner view, or you'll get auto layout errors when you run the app.
The response from rdelmar was enough for me to get this working, but I'll add a few things. With auto layout on, there is no need to set the banner's size with setAutoresizingMask:UIViewAutoresizingFlexibleWidth (and currentContentSizeIdentifier is deprecated in iOS 6). Just create the banner object and then pin it into position using the procedure outlined by rdelmar and auto layout takes care of the horizontal sizing.
Here are the constraints I used:
// pin sides to superview
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[_bannerView]-0-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_bannerView)]];
// set height to a constant
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[_bannerView(==66)]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_bannerView)]];
// pin contentView to bannerView with 0 length constraint
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[_contentView]-0-[_bannerView]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_contentView,_bannerView)]];
I was concerned about setting a height constraint because the height of the banner will change depending on platform and/or orientation. But it doesn't seem to make any difference what value I set for the height constraint - the banner is always shown with the correct height, so I don't even bother setting it. I am assuming this because there is an intrinsic sizing to the height of the ad banners.
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.