user3126427
user3126427

Reputation: 855

How to change the TextView height dynamically to a threshold and then allow scrolling?

I have a TextView that has a constraint of min height of 33. The scroll is disabled from the storyboard. The TextView should increase in height based on the content until it reaches the max height of 100. Then I changes the scrollEnabled to true and the height of the TextView to max height of 100, but the height changes to the 33. How can I fix this problem?

import UIKit

class ViewController: UIViewController, UITextViewDelegate {

    @IBOutlet weak var messageTextView: UITextView!
    let messageTextViewMaxHeight: CGFloat = 100
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.messageTextView.delegate = self
    }
    
    func textViewDidChange(textView: UITextView) {
        
        if textView.frame.size.height >= self.messageTextViewMaxHeight {
            textView.scrollEnabled = true
            textView.frame.size.height = self.messageTextViewMaxHeight
        } else {
            textView.scrollEnabled = false
        }
    }
}

enter image description here

Upvotes: 11

Views: 9524

Answers (8)

devjme
devjme

Reputation: 720

The accepted answer for this question and also this answer are both great solutions for this. Also this one

But I ended up with this one one with ONE CHANGE!! THE NEW SELF SIZING METHOD FOR iOS 16 invalidateIntrinsicContentSize():

 override func adjustTextViewHeight(textView: UITextView) {
    let fixedWidth = initialTextWidth ?? txtFMessage.frame.size.width
    
    let newSize = txtFMessage.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
    
    if newSize.height > txtFMessage.frame.size.height {
        
        txtFMessage.isScrollEnabled = false
        
        cnstTxtMessageHeight.constant = newSize.height
        
       // self.layoutSubviews()
        
        invalidateIntrinsicContentSize()
    }
}

Upvotes: 0

Kivia Martins Brito
Kivia Martins Brito

Reputation: 281

My solution was, on my CostumeView I added a constrain to the UITextView:

 textInput.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
  textInput.topAnchor.constraint(equalTo: divider.bottomAnchor, constant: 5),
  textInput.heightAnchor.constraint(lessThanOrEqualToConstant: 100), // very important
  textInput.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor),
  textInput.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor)])

The "textInput.heightAnchor.constraint(lessThanOrEqualToConstant: 100)" is what it is doing the trick. So I set the UITextView to isScrollEnabled = false:

textView.isScrollEnabled = false

Then on my ViewController I added the UITextViewDelegate :

extension InboxMsgVC: UITextViewDelegate {

And I finally added the method below:

func textViewDidChange(_ textView: UITextView) {
if textView.contentSize.height >= 100 {
  textView.isScrollEnabled = true
} else {
  textView.isScrollEnabled = false 
}

}

That it is, it works fine for me , Swift 5.

Upvotes: 0

Sam Conrad
Sam Conrad

Reputation: 13

Create a class that inherits from UITextView and add the following into the class:

class CustomTextView: UITextView, UITextViewDelegate {

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)

        delegate = self
    }

    var maxHeight: CGFloat = 200

    override var intrinsicContentSize: CGSize {
        var size = super.intrinsicContentSize
        if size.height > maxHeight {
            size.height = maxHeight
            isScrollEnabled = true
        } else {
            isScrollEnabled = false
        }
        return size
    }

    override var text: String! {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }

    func textViewDidChange(_ textView: UITextView) {
        invalidateIntrinsicContentSize()
    }

}

Note: - You can initialize maxHeight to infinity and set maxHeight after creating the CustomTextView. This class can be reused anywhere it is needed in the app, and the max height can be modified for different scenarios.

Upvotes: 1

wardw
wardw

Reputation: 826

This follows a similar approach to the accepted answer but ensures the textView is fully constrained in both height states.

(There's a bug in the accepted answer - using a height constraint with a <= relation is insufficient to fully constrain the textView when scrolling is enabled, since in this case the view provides no intrinsicContentSize. You can see this in IB (with scrolling disabled), or at runtime via view debugging.)

This is all that's necessary:

// In IB, set the relation to `=` and the constant to your desired threshold point
// Notice this is a strong reference (since the constraint may get deactivated) 
@IBOutlet var textViewHeightConstraint: NSLayoutConstraint!

func textViewDidChange(textView: UITextView)
{       
    let isOversize = textView.contentSize.height >= textViewHeightConstraint.constant
    textViewHeightConstraint.isActive = isOversize
    textView.isScrollEnabled = isOversize
}

There's no need to set frames manually, since in both cases auto-layout has us covered.

Upvotes: 5

Jigar Oza
Jigar Oza

Reputation: 665

It seems your code requires two changes, and it will work fine.

  1. Instead of min height of constraint provide max height of 100:

Height Constraint for TextView

  1. Change code as below:

     import UIKit
    
    class ViewController: UIViewController, UITextViewDelegate
    {
        @IBOutlet weak var messageTextView: UITextView!
    
        let messageTextViewMaxHeight: CGFloat = 100
        override func viewDidLoad()
        {
            super.viewDidLoad()
            messageTextView.delegate = self
        }
    
        func textViewDidChange(textView: UITextView)
        {
            if textView.contentSize.height >= self.messageTextViewMaxHeight
            {
                textView.scrollEnabled = true
            }
            else
                {
                textView.frame.size.height = textView.contentSize.height
                textView.scrollEnabled = false // textView.isScrollEnabled = false for swift 4.0
            }
        }
    }
    

Upvotes: 11

arsenius
arsenius

Reputation: 13246

After many many hours of problems with textviews in table view cells, this was the solution that worked for me. I'm using Masonry, but the constraints could be created in IB as well.

Note that the textview delegate is not used. This is advantageous because it doesn't matter whether you change the text programmatically or via user input. Either way layoutSubviews gets called whenever a text view changes its contents.

If you want to have this directly in your view controller you could use viewDidLayoutSubviews instead of layoutSubviews.

@implementation TextViewCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self){
        UITextView *textView = [[UITextView alloc] initWithFrame:CGRectZero];
        textView.scrollEnabled = NO;
        self.textView = textView;
        [self.contentView addSubview:self.textView];

        [self.textView mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.leading.equalTo(self.contentView.mas_leadingMargin);
            make.trailing.equalTo(self.contentView.mas_trailingMargin);
            make.top.equalTo(self.contentView.mas_topMargin);
            make.bottom.equalTo(self.contentView.mas_bottomMargin);
            make.height.lessThanOrEqualTo(@100.0).with.priorityHigh();
            make.height.greaterThanOrEqualTo(@30.0).with.priorityHigh();
        }];
    }
    return self;
}


- (void)layoutSubviews{
    CGSize size = [self.textView sizeThatFits:CGSizeMake(self.textView.bounds.size.width, 10000)];
    self.textView.scrollEnabled = size.height > 100.0;
    [super layoutSubviews];
}

@end

Upvotes: 0

jhd
jhd

Reputation: 1253

Maybe the code below is a little better.

import UIKit

let width = UIScreen.mainScreen().bounds.width

class ViewController: UIViewController, UITextViewDelegate {

    var messageTextView: UITextView!
    let messageTextViewMaxHeight: CGFloat = 100

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.redColor()
        messageTextView = UITextView(frame: CGRectMake(0, 200 - 33, width, 33))
        messageTextView.backgroundColor = UIColor.whiteColor()
        view.addSubview(messageTextView)
        self.messageTextView.delegate = self
    }

    //
    let bottomY: CGFloat = 200

    func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
        if text == "\n"{
            let formerRect = textView.frame
            let contentSize = textView.contentSize
            if contentSize.height < formerRect.height{
                //
            }
            else{
                if contentSize.height >= messageTextViewMaxHeight{
                    //
                }
                else{
                    textView.frame = CGRectMake(formerRect.origin.x, bottomY - contentSize.height, width, contentSize.height)
                }
            }
        }
        return true
    }

}

Upvotes: 0

jhd
jhd

Reputation: 1253

For an easy way to solve your problem is to change the frame of the textView whenever textViewDidChange invokes. UITextView actually is a UIScrollView. If you have to use constraint, you have to change the constant of the constraint. Here is my code:

import UIKit

let width = UIScreen.mainScreen().bounds.width

class ViewController: UIViewController, UITextViewDelegate {

    var messageTextView: UITextView!
    let messageTextViewMaxHeight: CGFloat = 100

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.redColor()
        messageTextView = UITextView(frame: CGRectMake(0, 200 - 33, width, 33))
        messageTextView.backgroundColor = UIColor.whiteColor()
        view.addSubview(messageTextView)
        self.messageTextView.delegate = self
    }

    //
    let bottomY: CGFloat = 200

    func textViewDidChange(textView: UITextView) {
        let formerRect = textView.frame

        if formerRect.height > messageTextViewMaxHeight{

        }
        else{
            textView.frame = CGRectMake(formerRect.origin.x, bottomY - textView.contentSize.height, width, textView.contentSize.height)
        }
    }
} 

Upvotes: 0

Related Questions