9

I'm working on an app which uses a UITextView.

The UITextView should grow or shrink to fit its text, both vertically and horizontally. To do this, I'm overriding sizeToFit in a subclass, and I set the bounds like so:

- (void)sizeToFit {
    [self setBounds:(CGRect){.size = self.attributedText.size}];
}

The problem is that this size just doesn't reflect the correct size of the string, as the UITextView clips the text. I'm setting the edge insets to zero, so that shouldn't be an issue right?

At this point, I think it's a bug with NSAttributedString's size property, but the same thing happens if I use boundingRectWithSize:options:context:

[self setBounds:(CGRect){.size = [self.attributedText boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:0 context:nil].size}];

So maybe whatever code is doing layout calculations for NSAttributedString doesn't play nicely with UITextView's layout calculations.

Here is example project which demonstrates the issue.

Any ideas are welcome!

EDIT: I should also point out that the following doesn't work either:

- (void)sizeToFit {
    [self setBounds:(CGRect){.size = [self sizeThatFits:self.attributedText.size]}];
}
Tom Irving
  • 10,041
  • 6
  • 47
  • 63
  • For a regular text string I resorted to putting it in a dummy label, shrinking the label to fit, and then taking its dimensions. – Hot Licks Aug 31 '13 at 19:28

3 Answers3

4

Though not perfect, I ended up using:

- (void)sizeToFit {

    CGSize textSize = self.attributedText.size;
    CGSize viewSize = CGSizeMake(textSize.width + self.firstCharacterOrigin.x * 2,
                                 textSize.height + self.firstCharacterOrigin.y * 2);

    [self setBounds:(CGRect){.size = [self sizeThatFits:viewSize]}];
}

- (CGPoint)firstCharacterOrigin {

    if (!self.text.length) return CGPointZero;

    UITextRange * range = [self textRangeFromPosition:[self positionFromPosition:self.beginningOfDocument offset:0]
                                           toPosition:[self positionFromPosition:self.beginningOfDocument offset:1]];
    return [self firstRectForRange:range].origin;
}
Tom Irving
  • 10,041
  • 6
  • 47
  • 63
  • Please add a check if `self` does have a non-empty text and only then use your `firstCharacterOrigin`'s implementation, and add a `return CGPointZero` if not. See this http://stackoverflow.com/a/18548958/598057. – Stanislav Pankevich Aug 31 '13 at 14:58
1

I think you're right--I couldn't get -[NSAttributedString boundingRectWithSize:options:context: ] to return a value that worked correctly for all font sizes...

I did get a UILabel to correctly size and draw attributed strings however:

  • change your text view class to UILabel
  • use -setAttributedText: on the label
  • set label.numberOfLines = 0 (multiline)
  • in -layoutSubviews of your label's superview call -[ label sizeThatFits: ] to get the correct size for the label....

Worked for me...

EDIT: Here's my ViewController.m file:

@interface View : UIView
@property ( nonatomic, strong ) UITextView * textView ;
@property ( nonatomic, strong ) UISlider * fontSizeSlider ;
@end

@implementation View

-(void)layoutSubviews
{
    CGRect bounds = self.bounds ;

    self.textView.layer.anchorPoint = (CGPoint){ 0.0f, 0.5f } ;
    self.textView.layer.position = (CGPoint){ CGRectGetMinX( bounds ), CGRectGetMidY( bounds ) } ;
    self.textView.bounds = (CGRect){ .size = [ self.textView sizeThatFits:bounds.size ] } ;

    self.fontSizeSlider.frame = CGRectMake(5, CGRectGetMaxY(bounds) - 30, bounds.size.width - 10, 25) ;
}

@end

@interface ViewController ()

@end

@implementation ViewController

-(void)loadView
{
    self.view = [[ View alloc ] initWithFrame:CGRectZero ] ;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    NSMutableAttributedString * aString = [[ NSMutableAttributedString alloc ] initWithString:@"Some test text in a scaling text view" ] ;
    NSDictionary * attributes = @{ NSForegroundColorAttributeName : [ UIColor redColor ] } ;
    [ aString setAttributes:attributes range:(NSRange){ 5, 4 } ] ;

    textView = [[UITextView alloc] initWithFrame:CGRectZero];
    [textView setAttributedText:aString ];
    [textView setFont:[UIFont systemFontOfSize:18]];
    [textView setCenter:CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds))];

    ((View*)self.view).textView = textView ;
    [self.view addSubview:textView];
    [textView release];

    UISlider * fontSizeSlider = [[UISlider alloc] initWithFrame:CGRectMake(5, CGRectGetMaxY(self.view.bounds) - 30, self.view.bounds.size.width - 10, 25)];
    [fontSizeSlider addTarget:self action:@selector(fontSizeSliderDidChange:) forControlEvents:UIControlEventValueChanged];
    [fontSizeSlider setMinimumValue:5];
    [fontSizeSlider setMaximumValue:100];
    [fontSizeSlider setValue:textView.font.pointSize];
    ((View*)self.view).fontSizeSlider = fontSizeSlider ;
    [self.view addSubview:fontSizeSlider];
    [fontSizeSlider release];
}

- (void)fontSizeSliderDidChange:(UISlider *)sender {
    [ textView setFont:[textView.font fontWithSize:sender.value]];
    [ self.view setNeedsLayout ] ;
}

@end
nielsbot
  • 15,922
  • 4
  • 48
  • 73
  • Though the UILabel sizes correctly, it's not editable. Obviously, I could switch out the label for a UITextView on touch down, but if it's possible I'd like to avoid that route unless absolutely necessary. – Tom Irving Jan 27 '13 at 15:21
  • Sure, but that requires using both a UILabel and UITextView, which, as I said, I don't mind, but if there's a solution that just uses a UITextView, that would be great. – Tom Irving Jan 27 '13 at 19:57
  • From your edits, I think we're on difference pages here. I don't want the textView to be the size of the view, it should be the size it needs to fit the text, no larger and no smaller. – Tom Irving Jan 27 '13 at 20:19
  • That's exactly what it does.. it fits the width, but grows/shrinks in height. Try my code. – nielsbot Jan 27 '13 at 20:40
  • Right, your code does fit to the width of the view, which isn't the desired behaviour, it should be just large enough (both width and height) to fit the text, no more and no less. – Tom Irving Jan 27 '13 at 21:47
  • But that's the point, it doesn't work. See the example project, try scaling the font size. – Tom Irving Jan 28 '13 at 01:07
  • i guess keep narrowing it until it grows in height... I updated my code. – nielsbot Jan 28 '13 at 01:26
  • otherwise you'll have to use CoreText. `-boundingRectWithSize:options:context:` seems to be broken. – nielsbot Jan 28 '13 at 01:27
0

swift 3 solution for tom irvings solution above:

extension UITextView {
    func sizeToFitAttributedString() {
        let textSize = self.attributedText.size()
        let viewSize = CGSize(width: textSize.width + self.firstCharacterOrigin.x * 2, height: textSize.height + self.firstCharacterOrigin.y * 2)
        self.bounds.size = self.sizeThatFits(viewSize)
    }

    private var firstCharacterOrigin: CGPoint {
        if self.text.lengthOfBytes(using: .utf8) == 0 {
            return .zero
        }
        let range = self.textRange(from: self.position(from: self.beginningOfDocument, offset: 0)!,
                                     to: self.position(from: self.beginningOfDocument, offset: 1)!)
        return self.firstRect(for: range!).origin

    }
}
tungsten
  • 329
  • 2
  • 10