UITextView that expands to text using auto layout

I have a view that is laid out completely using auto layout programmatically. I have a UITextView in the middle of the view with items above and below it. Everything works fine, but I want to be able to expand UITextView as text is added. This should push everything below it down as it expands.

I know how to do this the “springs and struts” way, but is there an auto layout way of doing this? The only way I can think of is by removing and re-adding the constraint every time it needs to grow.

  • How to hide first section header in UITableView (grouped style)
  • What is the use of NSExpression?
  • Usage of MVVM in iOS
  • Cannot access NSUserDefaults using app groups one to another
  • Only on new iPad 3: wait_fences: failed to receive reply: 10004003
  • Dynamic UITableCellView height with cell indicator
  • 10 Solutions Collect From Internet About “UITextView that expands to text using auto layout”

    The view containing UITextView will be assigned its size with setBounds by AutoLayout. So, this is what I did. The superview is initially set up all the other constraints as they should be, and in the end I put one special constraint for UITextView’s height, and I saved it in an instance variable.

        _descriptionHeightConstraint = [NSLayoutConstraint constraintWithItem:_descriptionTextView
                                         attribute:NSLayoutAttributeHeight 
                                         relatedBy:NSLayoutRelationEqual 
                                            toItem:nil 
                                         attribute:NSLayoutAttributeNotAnAttribute 
                                        multiplier:0.f 
                                         constant:100];
    
        [self addConstraint:_descriptionHeightConstraint];
    

    In the setBounds method, I then changed the value of the constant.

    -(void) setBounds:(CGRect)bounds
    {
        [super setBounds:bounds];
    
        _descriptionTextView.frame = bounds;
        CGSize descriptionSize = _descriptionTextView.contentSize;
    
        [_descriptionHeightConstraint setConstant:descriptionSize.height];
    
        [self layoutIfNeeded];
    }
    

    Just got into this issue, I don’t know if I’m off topic, but if you set your UITextView to “not” scrollable:

    [textView setScrollEnabled:NO];
    

    it will be able to calculate an intrinsic size, thus allowing auto layout to move the other elements by itself, then it’s up to you to setup the constraints right.

    (on iOS 7)

    UITextView doesn’t provide an intrinsicContentSize, so you need to subclass it and provide one. To make it grow automatically, invalidate the intrinsicContentSize in layoutSubviews. If you use anything other than the default contentInset (which I do not recommend), you may need to adjust the intrinsicContentSize calculation.

    @interface AutoTextView : UITextView
    
    @end
    

    #import "AutoTextView.h"
    
    @implementation AutoTextView
    
    - (void) layoutSubviews
    {
        [super layoutSubviews];
    
        if (!CGSizeEqualToSize(self.bounds.size, [self intrinsicContentSize])) {
            [self invalidateIntrinsicContentSize];
        }
    }
    
    - (CGSize)intrinsicContentSize
    {
        CGSize intrinsicContentSize = self.contentSize;
    
        // iOS 7.0+
        if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0f) {
            intrinsicContentSize.width += (self.textContainerInset.left + self.textContainerInset.right ) / 2.0f;
            intrinsicContentSize.height += (self.textContainerInset.top + self.textContainerInset.bottom) / 2.0f;
        }
    
        return intrinsicContentSize;
    }
    
    @end
    

    I’ve found it’s not entirely uncommon in situations where you may still need isScrollEnabled set to true to allow a reasonable UI interaction. A simple case for this is when you want to allow an auto expanding text view but still limit it’s maximum height to something reasonable in a UITableView.

    Here’s a subclass of UITextView I’ve come up with that allows auto expansion with auto layout but that you could still constrain to a maximum height and which will manage whether the view is scrollable depending on the height. By default the view will expand indefinitely if you have your constraints setup that way.

    import UIKit
    
    class FlexibleTextView: UITextView {
        // limit the height of expansion per intrinsicContentSize
        var maxHeight: CGFloat = 0.0
        private let placeholderTextView: UITextView = {
            let tv = UITextView()
    
            tv.translatesAutoresizingMaskIntoConstraints = false
            tv.backgroundColor = .clear
            tv.isScrollEnabled = false
            tv.textColor = .disabledTextColor
            tv.isUserInteractionEnabled = false
            return tv
        }()
        var placeholder: String? {
            get {
                return placeholderTextView.text
            }
            set {
                placeholderTextView.text = newValue
            }
        }
    
        override init(frame: CGRect, textContainer: NSTextContainer?) {
            super.init(frame: frame, textContainer: textContainer)
            isScrollEnabled = false
            autoresizingMask = [.flexibleWidth, .flexibleHeight]
            NotificationCenter.default.addObserver(self, selector: #selector(UITextInputDelegate.textDidChange(_:)), name: Notification.Name.UITextViewTextDidChange, object: self)
            placeholderTextView.font = font
            addSubview(placeholderTextView)
    
            NSLayoutConstraint.activate([
                placeholderTextView.leadingAnchor.constraint(equalTo: leadingAnchor),
                placeholderTextView.trailingAnchor.constraint(equalTo: trailingAnchor),
                placeholderTextView.topAnchor.constraint(equalTo: topAnchor),
                placeholderTextView.bottomAnchor.constraint(equalTo: bottomAnchor),
            ])
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override var text: String! {
            didSet {
                invalidateIntrinsicContentSize()
                placeholderTextView.isHidden = !text.isEmpty
            }
        }
    
        override var font: UIFont? {
            didSet {
                placeholderTextView.font = font
                invalidateIntrinsicContentSize()
            }
        }
    
        override var contentInset: UIEdgeInsets {
            didSet {
                placeholderTextView.contentInset = contentInset
            }
        }
    
        override var intrinsicContentSize: CGSize {
            var size = super.intrinsicContentSize
    
            if size.height == UIViewNoIntrinsicMetric {
                // force layout
                layoutManager.glyphRange(for: textContainer)
                size.height = layoutManager.usedRect(for: textContainer).height + textContainerInset.top + textContainerInset.bottom
            }
    
            if maxHeight > 0.0 && size.height > maxHeight {
                size.height = maxHeight
    
                if !isScrollEnabled {
                    isScrollEnabled = true
                }
            } else if isScrollEnabled {
                isScrollEnabled = false
            }
    
            return size
        }
    
        @objc private func textDidChange(_ note: Notification) {
            // needed incase isScrollEnabled is set to true which stops automatically calling invalidateIntrinsicContentSize()
            invalidateIntrinsicContentSize()
            placeholderTextView.isHidden = !text.isEmpty
        }
    }
    

    As a bonus there’s support for including placeholder text similar to UILabel.

    You can do it through storyboard, just disable “Scrolling Enabled”:)

    StoryBoard

    You can also do it without subclassing UITextView. Have a look at my answer to How do I size a UITextView to its content on iOS 7?

    Use the value of this expression:

    [textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)].height
    

    to update the constant of the textView‘s height UILayoutConstraint.

    An important thing to note:

    Since UITextView is a subclass of UIScrollView, it is subject to the automaticallyAdjustsScrollViewInsets property of UIViewController.

    If you are setting up the layout and the TextView is the the first subview in a UIViewControllers hierarchy, it will have its contentInsets modified if automaticallyAdjustsScrollViewInsets is true sometimes causing unexpected behaviour in auto layout.

    So if you’re having problems with auto layout and text views, try setting automaticallyAdjustsScrollViewInsets = false on the view controller or moving the textView forward in the hierarchy.

    Here’s a solution for people who prefer to do it all by auto layout:

    In Size Inspector:

    1. Set content compression resistance priority vertical to 1000.

    2. Lower the priority of constraint height by click “Edit” in Constraints. Just make it less than 1000.

    enter image description here

    In Attributes Inspector:

    1. Uncheck “Scrolling Enabled”

    BTW, I built an expanding UITextView using a subclass and overriding intrinsic content size. I discovered a bug in UITextView that you might want to investigate in your own implementation. Here is the problem:

    The expanding text view would grow down to accommodate the growing text if you type single letters at a time. But if you paste a bunch of text into it, it would not grow down but the text would scroll up and the text at the top was out of view.

    The solution:
    Override setBounds: in your subclass. For some unknown reason, the pasting caused the bounds.origin.y value to be non-zee (33 in every case that I saw). So I overrode setBounds: to always set the bounds.origin.y to zero. Fixed the problem.

    Obj C:
    
    #import <UIKit/UIKit.h>
    
    @interface ViewController : UIViewController
    
    @property (nonatomic) UITextView *textView;
    @end
    
    
    
    #import "ViewController.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    @synthesize textView;
    
    - (void)viewDidLoad{
        [super viewDidLoad];
        [self.view setBackgroundColor:[UIColor grayColor]];
        self.textView = [[UITextView alloc] initWithFrame:CGRectMake(30,10,250,20)];
        self.textView.delegate = self;
        [self.view addSubview:self.textView];
    }
    
    - (void)didReceiveMemoryWarning{
        [super didReceiveMemoryWarning];
    }
    
    - (void)textViewDidChange:(UITextView *)txtView{
        float height = txtView.contentSize.height;
        [UITextView beginAnimations:nil context:nil];
        [UITextView setAnimationDuration:0.5];
    
        CGRect frame = txtView.frame;
        frame.size.height = height + 10.0; //Give it some padding
        txtView.frame = frame;
        [UITextView commitAnimations];
    }
    
    @end