Chris
Chris

Reputation: 282

Toggle Enabling UITextView Scroll After Max Number of Lines

I have a uitextview subview in an inputContainerView as it may appear in a messaging app.

Programmatically, on load, the height anchor of the uitextview is set when the inputContainerView is initialized. This constraint works great in increasing the container height as more lines of text are typed.

My goal is to cap the height of the uitextview after reaching X number of lines where any lines typed thereafter are scrollable. And likewise, when the number of lines falls below the max, it returns to its original form - not scrollable and auto-sizing height based on content.

After multiple trials and research, I've managed to get the height to fixate once hitting the max number of lines, but I cannot seem to figure out how to return it to its original form once the number of lines have fallen below the max. It appears the uitextview height stays stuck at the height the number of lines have maxed on.

Below is relevant code:

//Container View Initialization, onLoad Frame Height is set to 60
override init(frame: CGRect) {
super.init(frame: frame)
autoresizingMask = .flexibleHeight

addSubview(chatTextView)
textView.heightAnchor.constraint(greaterThanOrEqualToConstant: frame.height - 20).isActive = true
}

//TextView Delegate

var isTextViewOverMaxHeight = false
var textViewMaxHeight: CGFloat = 0.0

func textViewDidChange(_ textView: UITextView) {
    let numberOfLines = textView.contentSize.height/(textView.font?.lineHeight)!

    if Int(numberOfLines) > 5 {

        if !isTextViewOverMaxHeight {
            containerView.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 40).isActive = false
            containerView.textView.heightAnchor.constraint(equalToConstant: 
            containerView.textView.frame.height).isActive = true
            containerView.textView.isScrollEnabled = true                

            textViewMaxHeight = containerView.textView.frame.height
            isTextViewOverMaxHeight = true
        }
    } else {

        if isTextViewOverMaxHeight {
            containerView.textView.heightAnchor.constraint(equalToConstant: textViewMaxHeight).isActive = false
            containerView.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 40).isActive = true
            containerView.textView.isScrollEnabled = false                

            isTextViewOverMaxHeight = false
        }
    }
}

Upvotes: 0

Views: 1633

Answers (2)

Andrey Volobuev
Andrey Volobuev

Reputation: 958

It's also possible to disable initial textView's isScrollEnabled and then implement text view resizing like this:

private var heightConstraint: NSLayoutConstraint?
private let inputLinesScrollThreshold = 5

func textViewDidChange(_ textView: UITextView) {
    
    let isConstraintActive = heightConstraint.flatMap { $0.isActive } ?? false
    
    let lineHeight = textView.font?.lineHeight ?? 1
    let linesCount = Int(textView.contentSize.height / lineHeight)
    
    if isConstraintActive == false {
        heightConstraint = textView.heightAnchor.constraint(equalToConstant: textView.frame.height)
        heightConstraint?.isActive = true
        textView.isScrollEnabled = true
    } else {
        heightConstraint?.constant = linesCount > inputLinesScrollThreshold ?
            lineHeight * CGFloat(inputLinesScrollThreshold) : textView.contentSize.height
    }
    textView.layoutIfNeeded()
}

Upvotes: 0

Sandeep Bhandari
Sandeep Bhandari

Reputation: 20369

You have overcomplicated a simple problem at hand, here is how can you achieve what you want

class ViewController: UIViewController {
    var heightConstraint: NSLayoutConstraint!
    var heightFor5Lines: CGFloat = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        let textView = UITextView(frame: CGRect.zero)
        textView.delegate = self
        textView.backgroundColor = UIColor.red
        textView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(textView)
        textView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        textView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
        textView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
        heightConstraint = textView.heightAnchor.constraint(equalToConstant: 30.0)
        heightConstraint.isActive = true
    }
}


extension ViewController: UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        let numberOfLines = textView.contentSize.height/(textView.font?.lineHeight)!

        if Int(numberOfLines) > 5 {
            self.heightConstraint.constant = heightFor5Lines
        } else {
            if Int(numberOfLines) == 5 {
                self.heightFor5Lines = textView.contentSize.height
            }
            self.heightConstraint.constant = textView.contentSize.height
        }
        textView.layoutIfNeeded()
    }
}

enter image description here

How this works?:

Its simple, your textView starts off with some height (height constraint is necessary here because I haven't neither disabled its scroll nor have I provided bottom constraint, so it does not have enough data to evaluate its intrinsic height) and every time text changes you check for number of lines, as long as number of lines is less than threshold number of lines you keep increasing the height constraint of your textView so that its frame matches the contentSize (hence no scrolling) and once it hits the expected number of lines, you restrict height, so that textView frame is less than the actual content size, hence scrolls automatically

Hope this helps

Upvotes: 10

Related Questions