How do I size a UITextView to its content on iOS 7?
Asked Answered
A

14

65

I've been using the accepted answer here for years.

On iOS 7, the contentSize.height becomes the frame.height-8, regardless of text content.

What's a working method to adjust the height on iOS 7?

Affenpinscher answered 27/9, 2013 at 11:24 Comment(1)
I am running into the same issue. I see that they added textContainer to UIView that has a size but I am currently see it's size be inaccurate.Dismantle
A
55

I favor this minimal code change: Just add these two lines after addSubview and before grabbing the height of the frame

...
[scrollView1 addSubview: myTextView];

    [myTextView sizeToFit]; //added
    [myTextView layoutIfNeeded]; //added

CGRect frame = myTextView.frame;
...

This is tested backwards compatible with iOS 6. NOTE that it shrink-wraps the width. If you're just interested in the height and have a fixed width, just grab the new height but set the original width, and it works just as before on both iOS 6 and 7.

(Speculation: it does size to fit on iOS 7 as well, but the layout is updated later or in a separate thread, and that this forces the layout immediately so that its frame is updated in time for using its height value a few lines later in the same thread.)

NOTES:

1) You might or might not have implemented the outer container resize this way. It does seem to be a common snippet, though, and I've used it in my projects.

2) Since sizeToFit seems to work as expected on iOS 7, you likely don't need the premature addSubView. Whether it will still work on iOS 6 then is untested by me.

3) Speculation: The extra layoutIfNeeded mid-thread might be costly. The alternative as I see it is to resize the outer container on the layout callback (fired or not depending on if the OS decides whether layout is needed or not) where the outer container resize will cause another layout update. Both updates might be combined with other layout updates to be more efficient. If you do have such a solution and you can show that it is more efficient, add it as answer and I'll be sure to mention it here.

Affenpinscher answered 30/9, 2013 at 9:21 Comment(3)
It resizes textview but I still didn't manage to resize textview's parent element with this in iOS 7, can you please share more details ?Addington
No idea, too little information. But if you write a question with the code that doesn't work I could have a look.Affenpinscher
@JannieT since I posted the answer I've used it in every app I've updated on the app store, I guess five or so. Some text views were single line, some were multiline. I may be updating another today, could have a look on the very latest OS 7.1.x.Affenpinscher
S
33

Since I'm using Auto Layout, I use the value of [textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)].height to update the constant of the textView's height UILayoutConstraint.

Spanker answered 17/10, 2013 at 20:17 Comment(5)
thanks, this is useful. was hoping to dynamically resize as I type though. I guess the best I can ask for is to put the sizing logic in the delegate's textFieldDidEndEditing ?Carlsen
MattDiPasquale, There should I put these code? Into viewDidLoad, layoutSubviews or else?Wingo
@AlexanderVolkov layoutSubviews or viewWillAppear.Sapp
This NSLayoutConstraint is being configured with a constant that exceeds internal limits. A smaller value will be substituted, but this problem should be fixed. Break on void _NSLayoutConstraintNumberExceedsLimit() to debug. This will be logged only once. This may break in the future. -[<_UITextTiledLayer: 0x7fd9a8dcfac0> display]: Ignoring bogus layer size (375.000000, 1000000000.000000), contentsScale 2.000000, backing store size (750.000000, 2000000000.000000)Gretchengrete
@mattdipasquale : No idea why this hasn't been marked as accepted answer :) But u sir :) Saved my day with this piece of code :) Hence up voted :)Zeidman
G
17

I use an adapted version of madmik's answer that eliminates the fudge factor:

- (CGFloat)measureHeightOfUITextView:(UITextView *)textView
{
    if ([textView respondsToSelector:@selector(snapshotViewAfterScreenUpdates:)])
    {
        // This is the code for iOS 7. contentSize no longer returns the correct value, so
        // we have to calculate it.
        //
        // This is partly borrowed from HPGrowingTextView, but I've replaced the
        // magic fudge factors with the calculated values (having worked out where
        // they came from)

        CGRect frame = textView.bounds;

        // Take account of the padding added around the text.

        UIEdgeInsets textContainerInsets = textView.textContainerInset;
        UIEdgeInsets contentInsets = textView.contentInset;

        CGFloat leftRightPadding = textContainerInsets.left + textContainerInsets.right + textView.textContainer.lineFragmentPadding * 2 + contentInsets.left + contentInsets.right;
        CGFloat topBottomPadding = textContainerInsets.top + textContainerInsets.bottom + contentInsets.top + contentInsets.bottom;

        frame.size.width -= leftRightPadding;
        frame.size.height -= topBottomPadding;

        NSString *textToMeasure = textView.text;
        if ([textToMeasure hasSuffix:@"\n"])
        {
            textToMeasure = [NSString stringWithFormat:@"%@-", textView.text];
        }

        // NSString class method: boundingRectWithSize:options:attributes:context is
        // available only on ios7.0 sdk.

        NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
        [paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping];

        NSDictionary *attributes = @{ NSFontAttributeName: textView.font, NSParagraphStyleAttributeName : paragraphStyle };

        CGRect size = [textToMeasure boundingRectWithSize:CGSizeMake(CGRectGetWidth(frame), MAXFLOAT)
                                                  options:NSStringDrawingUsesLineFragmentOrigin
                                               attributes:attributes
                                                  context:nil];

        CGFloat measuredHeight = ceilf(CGRectGetHeight(size) + topBottomPadding);
        return measuredHeight;
    }
    else
    {
        return textView.contentSize.height;
    }
}
Gimpel answered 21/10, 2013 at 11:26 Comment(3)
I can solved content height of UITextView is iOS 7 from tames code. Thanks.Abdullah
Doesn't adapt when the user hits return. New line clipped until you type in text.Fimble
works great for me, thanks :) to avoid line clipped, simply use [self.textView scrollRangeToVisible:NSMakeRange(0,0)]; when detected different line heightTruesdale
A
16

Based on other answers, I made it work(in Swift). This solves the problem with newline character.

textView.sizeToFit()
textView.layoutIfNeeded()
let height = textView.sizeThatFits(CGSizeMake(textView.frame.size.width, CGFloat.max)).height
textView.contentSize.height = height

Auto Layout is needed.

Atlantis answered 11/1, 2015 at 5:28 Comment(0)
B
15

If you're using Auto Layout, you could create a trivial UITextView subclass that self-sizes the text view height to fit the content:

@interface ContentHeightTextView : UITextView

@end

@interface ContentHeightTextView ()
@property (nonatomic, strong) NSLayoutConstraint *heightConstraint;
@end

@implementation ContentHeightTextView

- (void)layoutSubviews
{
    [super layoutSubviews];

    CGSize size = [self sizeThatFits:CGSizeMake(self.bounds.size.width, FLT_MAX)];

    if (!self.heightConstraint) {
        self.heightConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:1.0f constant:size.height];
        [self addConstraint:self.heightConstraint];
    }

    self.heightConstraint.constant = size.height;

    [super layoutSubviews];
}

@end

Of course, the text view's width and position must be defined by additional constraints configured elsewhere in the program.

If you create this custom text view in IB, give the text view a height constraint in order to satisfy Xcode; just make sure the height constraint created in IB is merely a placeholder (i.e., tick the box that says "Remove at build time").

enter image description here

An alternative way to implement the UITextView subclass is as follows (this implementation might qualify as best practice):

@interface ContentHeightTextView ()
@property (nonatomic, strong) NSLayoutConstraint *heightConstraint;
@end

@implementation ContentHeightTextView

- (void)layoutSubviews
{
    [super layoutSubviews];

    [self setNeedsUpdateConstraints];
}

- (void)updateConstraints
{
    CGSize size = [self sizeThatFits:CGSizeMake(self.bounds.size.width, FLT_MAX)];

    if (!self.heightConstraint) {
        self.heightConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:1.0f constant:size.height];
        [self addConstraint:self.heightConstraint];
    }

    self.heightConstraint.constant = size.height;
    [super updateConstraints];
}

@end
Biologist answered 23/2, 2014 at 8:25 Comment(1)
I think subclassing UITextView and working with height constraint is the best solution when dealing with auto-layout.Thank you.Xenophobe
O
4

If you are using auto-layout, you can use the following UITextView subclass that adds an intrinsic height:

@implementation SelfSizingTextView

- (void)setText:(NSString *)text
{
    [super setText:text];
    [self invalidateIntrinsicContentSize];
}

- (void)setFont:(UIFont *)font
{
    [super setFont:font];
    [self invalidateIntrinsicContentSize];
}

- (CGSize)intrinsicContentSize
{
    CGFloat width = self.frame.size.width;
    CGSize size = [self sizeThatFits:CGSizeMake(width, MAXFLOAT)];
    return CGSizeMake(UIViewNoIntrinsicMetric, size.height);
}

@end
Osculum answered 5/12, 2014 at 22:8 Comment(2)
It worked for me on iOS 7.0. But didn't check on iOS > 7.0. Thanks @phatmann.Forcer
The docs for intrinsicContentSize say: 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. So my code is not really kosher. On iOS 7 it worked, but it is not reliable on iOS 8. If you are using self-sizing cells on iOS 8, then you can use preferredLayoutAttributesFittingAttributes:Osculum
D
3

this method seems to work.

// Code from apple developer forum - @Steve Krulewitz, @Mark Marszal, @Eric Silverberg
- (CGFloat)measureHeight
{
if ([self respondsToSelector:@selector(snapshotViewAfterScreenUpdates:)])
{
    CGRect frame = internalTextView.bounds;
    CGSize fudgeFactor;
    // The padding added around the text on iOS6 and iOS7 is different.
    fudgeFactor = CGSizeMake(10.0, 16.0);

    frame.size.height -= fudgeFactor.height;
    frame.size.width -= fudgeFactor.width;

    NSMutableAttributedString* textToMeasure;
    if(internalTextView.attributedText && internalTextView.attributedText.length > 0){
        textToMeasure = [[NSMutableAttributedString alloc] initWithAttributedString:internalTextView.attributedText];
    }
    else{
        textToMeasure = [[NSMutableAttributedString alloc] initWithString:internalTextView.text];
        [textToMeasure addAttribute:NSFontAttributeName value:internalTextView.font range:NSMakeRange(0, textToMeasure.length)];
    }

    if ([textToMeasure.string hasSuffix:@"\n"])
    {
        [textToMeasure appendAttributedString:[[NSAttributedString alloc] initWithString:@"-" attributes:@{NSFontAttributeName: internalTextView.font}]];
    }

    // NSAttributedString class method: boundingRectWithSize:options:context is
    // available only on ios7.0 sdk.
    CGRect size = [textToMeasure boundingRectWithSize:CGSizeMake(CGRectGetWidth(frame), MAXFLOAT)
                                              options:NSStringDrawingUsesLineFragmentOrigin
                                              context:nil];

    return CGRectGetHeight(size) + fudgeFactor.height;
}
else
{
    return self.internalTextView.contentSize.height;
}
}
Dismantle answered 29/9, 2013 at 22:58 Comment(0)
P
1

If you're using iOS 7+, you can just turn on auto layout, pin each of the sides of the text view to the edge of its parent view, and it works fine. No additional code needed.

Picador answered 8/3, 2015 at 0:5 Comment(0)
F
1

Not sure if this was always the case but the following is true since at least iOS 10.

UITextView implements the intrinsicContentSize property if scrollEnabled == NO. That means you just need to make sure the width of the text view is constrained enough and then you can use the intrinsic content height (either via Auto Layout content hugging/compression resistance priorities or directly using the value during manual layout).

Unfortunately, this behavior is not documented. Apple could have easily saved us all some headaches… no need for an extra height constraint, subclassing, etc.

Foamflower answered 23/1, 2021 at 16:43 Comment(0)
G
0

In iOS 8 you'll in inherit some content offset from the parent, which you need to get rid of as well.

A subclass example

// Originally from https://github.com/Nikita2k/resizableTextView

#import "ResizableTextView.h"

@implementation ResizableTextView

- (void) updateConstraints {

    // calculate contentSize manually
    // ios7 doesn't calculate it before viewDidAppear and we'll get here before
    CGSize contentSize = [self sizeThatFits:CGSizeMake(self.frame.size.width, FLT_MAX)];

    // set the height constraint to change textView height
    [self.constraints enumerateObjectsUsingBlock:^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) {
        if (constraint.firstAttribute == NSLayoutAttributeHeight) {
            constraint.constant = contentSize.height;
            *stop = YES;
        }
    }];

    [super updateConstraints];
}

- (void)setContentOffset:(CGPoint)contentOffset
{
    // In iOS 8 we seem to be inheriting the content offset from the parent.
    // I'm not interested
}

@end
Giustino answered 16/9, 2014 at 11:57 Comment(0)
E
0

In storyboard, if using constraints, make sure you are constrained to your superview in the 'ruler' tab of the right-hand pane on xcode for the UITextView. My problem was that I had a constraint of -80 pts on the 'Trailing space to'.

enter image description here

Espousal answered 25/11, 2014 at 16:56 Comment(0)
C
0

Guys using autolayout and your sizetofit isn't working, then please check your width constraint once. If you had missed the width constraint then the height will be accurate.

No need to use any other API. just one line would fix all the issue.

[_textView sizeToFit];

Here, I was only concerned with height, keeping the width fixed and had missed the width constraint of my TextView in storyboard.

And this was to show up the dynamic content from the services.

Hope this might help..

Comatose answered 2/1, 2015 at 7:2 Comment(0)
T
0

I wrote a category over UITextView:

- (CGSize)intrinsicContentSize {
    return self.contentSize;
}

- (void)setContentSize:(CGSize)contentSize {
    [super setContentSize:contentSize];
    [self invalidateIntrinsicContentSize];
}

When UIKit sets its contentSize, UITextView adjusts its intrinsic content size. That plays nicely with autolayout.

Toothlike answered 30/3, 2016 at 16:14 Comment(0)
I
0

The answer given by bilobatum worked perfectly With auto layout, i.e subclassing the textview.

If you want to limit the height of the text view add another constraint (I added it using storyboard i.e. height <= 166 (height as per your need))

Then inside subclass reduce the priority of height constraint to 750 (self.heightConstraint.priority = 750) to avoid conflict between height constraint added in subclass and height constraint added on storyboard.

Inhesion answered 6/3, 2017 at 8:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.