Change ScrollView height based on content of the View - ios

I parse some Json Data and I show it using various UITextView, for now I used a ScrollView created via Storyboard that has a height of 1000px but sometimes I can't show the whole data since it's too long to fit the ScrollView, how can I update it programmatically in order to have a height based on the content the view has to show?

You may calculate the height of text:
CGRect textRect = [#"your text" boundingRectWithSize:CGSizeMake(yourScrollView.width, CGFLOAT_MAX)
options:(NSStringDrawingUsesLineFragmentOrigin)
attributes:#{NSFontAttributeName:[UIFont fontWithName:#"HelveticaNeue" size:18]}
context:nil];
And then update scrollView contentSize
[yourScrollView setContentSize:CGSizeMake(yourScrollView.frame.size.width,textRect.size.height)];

Go to your storyboard and remove all height constraint of your UITextViews. In order to avoid warnings in the storyboard, select Intrinsic Size -> Place Holder for each UITextView in your ScrollView. The option is located in the Size Inspector (right pane). This will tell XCode know that you don't want to specify a height, you rather want them to be based on its intrinsicContentSize.
Finally, create a new class 'FitContentTextView' that inherits from UITextView. In the storyboard make all your UITextView's that you want to have enough size to fit content of type FitContentTextView
- (void)setText:(NSString *)text {
[self setScrollEnabled:YES];
[super setText:text];
[self invalidateIntrinsicContentSize];
}
- (CGSize)intrinsicContentSize {
CGSize size = [self contentSize];
return size;
}
The setScrollEnabled thing in setText is important. The method contentSize only returns enough size to fit all text if srolling is enabled. Otherwise, will return the the TextView's bounds.
Also, calling invalidateIntrinsicContentSize every time we set the text makes sure autolayout will call the method intrinsicContentSize before drawing the TextView, and add this methods to it.
Lastly, remember to pin all ScrollView's subviews vertically to its superview from top to bottom. This is the only way AutoLayout can update the contentSize of the SrollView to fit all subviews.

Related

How to get preferred AutoLayout height given a width for a collection view cell with multiline labels?

I'm building a list view with self-sizing in my app using UICollectionView. Getting UICollectionViewFlowLayout to do a vertical list layout is a pain, so I'm writing my own UICollectionViewLayout subclass to do it.
My cells look a lot like regular table view cells - an image view on the left, a couple of labels vertically stacked in the center, and an accessory view on the right. The labels can wrap to a few lines and grow in font size according to the system font size setting, which is why they need to be self-sizing. Constraints are sensible - left to image view, image view to label, label to accessory, accessory to right, label to top and bottom.
To size my cells via preferredLayoutAttributesFittingAttributes, I need to get the desired height of the cell given the width of the collection view. I'd expect to use systemLayoutSizeFittingSize:horizontalPriority:verticalPriority: for this, passing a size like {desiredWidth, crazyLargeHeight}, UILayoutPriorityRequired for horizontal priority, and 1 for vertical priority. But the size I get back from systemLayoutFittingSize isn't sensible. The width is some previous width value from the cell (which doesn't necessarily match self.bounds.size.width or the passed-in layoutAttributes' size.width), and a height that works with that width. I also have tried passing a small height instead of a large one, and it still doesn't return the size it actually needs.
So how do I get the preferred height for the cell given a width value?
Sample code:
-(UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
UICollectionViewLayoutAttributes *result = [super preferredLayoutAttributesFittingAttributes:layoutAttributes];
CGSize exampleSize = CGSizeMake(layoutAttributes.size.width, 20);
CGSize size = [self systemLayoutSizeFittingSize:exampleSize withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:1];
result.size = size;
return result;
}
Turns out if you ask the cell's content view for a size instead of the cell itself, it works. And you have to give a sample height that is smaller than will be needed.
-(UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
UICollectionViewLayoutAttributes *result = [super preferredLayoutAttributesFittingAttributes:layoutAttributes];
CGSize exampleSize = CGSizeMake(layoutAttributes.size.width, 20); //magic number for example purposes...my cell will definitely be taller
CGSize size = [self.contentView systemLayoutSizeFittingSize:exampleSize withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:1];
result.size = size;
return result;
}
I'm guessing that the content view isn't pinned to the cell's bounds the way I expected. And this exact implementation will likely fall down if the content view is inset for some reason. But the key to solving this was the fact that having the cell contents pinned to the cell's content view does not mean that the cell itself will compute a size as you expect.

Automatically Grow a View's Height Based on It's Content

Situation
I have a (vertical) UIStackView containing both a plain UIView of height 50 (named sliderView) and a UILabel of height 36 defined in my storyboard. The label's alpha property is initially set to 0.0 to make it invisible.
In the controller's viewDidLoad I use UIViewController Containment to add another view controller's view to as a subview of sliderView. This new subview does not necessarily match sliderViews height. It might actually a fair bit taller.
At first, this setup looks fine. Once I make the label visible, I see that it still starts at a y-position of 50. So, the sliderView did not automatically stretch to use it's new child's height. Makes sense.
Question
I thought that I could easily just call sizeToFit on sliderView to make those two heights fit. Unfortunately, this does not seem to work. Am I misunderstanding something here? Thanks!
Use following method to get the CGSize required for NSString text.
- (CGSize)getHeightForText:(NSString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font {
CGSize size = CGSizeZero;
if (text) {
CGRect frame = [text boundingRectWithSize:CGSizeMake(widthValue, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:#{ NSFontAttributeName:font } context:nil];
size = CGSizeMake(frame.size.width, frame.size.height+20.0f);
}
return size;
}
Here width parameter is the width of your label and font is the font specified for your label. Call CGSize.height to get the height.

how make the UIView height equal to its subview's additional height, so that it can easily scroll till the end

I have a situation where i have following view layout.
-UIViewcontroller
-SCrollView
-UIView(outer)
-buttons
-labels
-UIView(inner)
-labels
-buttons
-buttons
the inner UIView height can be any longer as content inside is dynamically added.
So the main issue is i can not scroll till the end of the content, instead i can only scroll till the height of inner UIView.
NOTE - I am using auto layout
Any help is much appreciated.
Thanks
You should change the frame of your inner view dynamically based on the content you are adding. According to your inner view hierarchy, you have label and button. You might be setting some dynamic text on label and also doing some manipulation with button . Use the following method to get the height of the text ,
- (CGSize)getHeightForText:(NSString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font {
CGSize size = CGSizeZero;
if (text) {
CGRect frame = [text boundingRectWithSize:CGSizeMake(widthValue, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:#{ NSFontAttributeName:font } context:nil];
size = CGSizeMake(frame.size.width, frame.size.height);
}
return size;
}
Here font is the font you set to button and label. Call this method by passing the text added on label and button. CGSize has a height property , add height value of label and button. Now set this sum height to innerView by calling innerView.frame.size.height = <height>. This increase the height of your innerView based on content inside that.
Put all your buttons in the bottom inside a container view and add the following constraints to the outerView (assuming that the new container you've added is called "buttonContainer"):
A vertical spacing constraint between the bottom of innerView and the top of the buttonContainer.
A vertical spacing constraint between the bottom of the buttonContainer and the bottom of the outerView.
Horizontal spacing constraints between between the buttonContainer and outerView to force it to full width.
When using AutoLayout, you have to have enough constraints to properly define the contentSize of the scrollView.
For more information about using AutoLayout with UIScrollView, check out the following links by Apple:
Working with Scroll Views
UIScrollView and Autolayout

Autoresize UIPopoverController using autolayout

I'm trying to use autolayout to automatically resize my popover to fit it's contents. I have fixed popover width but to compute height i rely on systemLayoutSizeFittingSize: passing my predefined width and zero height e.g. CGSizeMake(190, 0).
ContentController* controller = [ContentController new];
CGSize preferredSize = [controller.view systemLayoutSizeFittingSize:CGSizeMake(190, 0)];
controller.preferredContentSize = preferredSize;
UIPopoverController* popover = [[UIPopoverController alloc] initWithContentViewController:controller];
//popover presentation.
So far, so well, my current ContentController view hierarchy is something like (setup in the Interface Builder):
UILabel - multiline header (dynamically resized)
|
UIImage with fixed width / height (static size)
|
UILabel - multiline body (dynamically resized)
Thus, i just plug in my header / body text, call systemLayoutSizeFittingSize and it returns valid size that fits all the content of the view.
The problem arises when i try to put my body label inside UIScrollView. (Both in the IB and in code).
From now on, systemLayoutSizeFittingSize will not take body label height into account and will return height for header + image.
I've setup all the constraints in the IB to support Pure Autolayout approach.
I've checked scrollview's content size - it is indeed equals to body label's intrinsic size, but scroll view's frame is squashed into 0.
I've checked and tried to reset label's maxPreferredLayoutWidth to the width of the content view, but it doesn't seems to affect anything.
I've set translatesAutoresizingMaskIntoConstraints on every view to NO, but it has no effect.
I've set hugging / compression resistance priorities of both label and it's scrollview to 1000, but no luck. Setting them on the container view doesn't work either.
Here are screenshots of my IB view setup:
My guess that is is somehow related to popover hosting views and their autolayout constraints, but i'm not sure.
I'm updating my labels via simple
_textContainerHeaderLabel.text = headerText;
_textContainerBodyTextLabel.text = bodyText;
[self.view setNeedsUpdateConstraints];
[self.view layoutSubviews];
So, the main question - how do i compute view's optimal fitting size via autolayout when it has UIScrollViews in it?
Finally found a solution for this case. Don't know how correct it is, but i did the following - I've added height inequality constraint (>=0) from label to it's scrollview.
The trick is to make this constraint's priority lower than label's compression resistance (vertical, in my case). This seems to to solve this problem.

UIView with dynamic height that uses intrinsicContentSize

I am trying to create a custom container view that has a UIImageView and a multiline UILabel as subviews. To make the view work nicely with autolayout, I am overriding intrinsicContentSize as below:
- (CGSize)intrinsicContentSize
{
return [self sizeThatFits:self.bounds.size];
}
The size calculated in sizeThatFits has the same width, and adjusts the height so that the label and image are not clipped. This works well, but I was surprised to see in the docs the following comment:
This intrinsic size must be independent of the content frame, because there’s no way to dynamically communicate a changed width to the layout system based on a changed height, for example.
If that is the case, what is the autolayout way to adjust the views current height based on its width and content? Should I be approaching this in a different way?
To answer my own question, it appears that there is not an autolayout suitable solution to this situation. Looking to UILabel for inspiration, the problem here has been solved with the addition of a property preferredMaxLayoutWidth, which can then be used as a constraining width during the intrinsic content size calculation. Any custom view would need to use something similar.
I think the doc means that, your containerView might have a placeHolderFrame as content frame.
intrinsic size should not be related to the content frame, but only to it's own subContent.
Your image and UILabel for example.
You should calculate both height and the width from the label and the image.
Which should be easy, since they all have intrinsic size.
Just my opinion...
I guess you could use UILabel's new preferredMaxLayoutWidth property to layout label correctly and use other approaches to layout other stuff.
Something like this:
- (void)layoutSubviews
{
...
[super layoutSubviews]; // get width from solved constraints
label.preferredMaxLayoutWidth = label.frame.size.width; // use it
[super layoutSubviews]; // update height of a label (probably intrinsicContentSize)
...
}
Align the bottom edge of the containing view with both the image and the label.
[self alignBottomEdgeWithView:labelView predicate:#"10"];
Details in http://code.dblock.org/ios-uiview-with-an-image-and-text-with-dynamic-height.

Resources