2

I have a UITextView called composeTextView that is right above the keyboard. This UITextView is inside a View called composeUIView. When the user enters text, the TextView adjusts in size to display all the text. My issue is that when I try to adjust the bottom constraint of composeUIView, the size of composeTextView stops being changed to fit its content. I don't understand what is happening here or how to fix it. If someone knows a fix I could use the help.

Long story short, I'm trying to show more lines of the UITextView as the user increases the number of lines they use, and then move the UIView that the UITextView is in up so that the extra lines displayed aren't covered by the keyboard.

class ChatViewController: UIViewController, UITextViewDelegate {

@IBOutlet weak var composeTextView: UITextView!
@IBOutlet weak var composeViewBottomConstraint: NSLayoutConstraint!

override func viewDidLoad() {
    super.viewDidLoad()

    composeTextView.delegate = self
}

func textViewDidChange(_ textView: UITextView) {

    let oldHeight = composeTextView.frame.height

    let fixedWidth = composeTextView.frame.size.width
    composeTextView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
    let newSize = composeTextView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
    var newFrame = composeTextView.frame
    newFrame.size = CGSize(width: max(newSize.width, fixedWidth), height: newSize.height)
    composeTextView.frame = newFrame

    let newHeight = composeTextView.frame.height

    let changeHeight = newHeight - oldHeight

    if changeHeight != 0 {
        self.composeViewBottomConstraint.constant += changeHeight
    }
}

EDIT 1:
As per Michael's suggestion, I made this change but it doesn't work consistently. If I hold down the backspace button and it starts deleting words at a time, it will end up with a smaller than normal TextView at the end. Also, if I paste a chunk of text or delete a chunk of text it doesn't respond properly:

var oldNumLines = CGFloat()

override func viewDidLoad() {
    super.viewDidLoad()

    composeTextView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)

    self.oldNumLines = composeTextView.contentSize.height / (composeTextView.font?.lineHeight)!

}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

    let newNumLines = composeTextView.contentSize.height / (composeTextView.font?.lineHeight)!

    print("oldNumLines: \(oldNumLines)")
    print("newNumLines: \(newNumLines)")

    let diffNumLines = newNumLines - oldNumLines
    print("diffNumLines: \(diffNumLines)")

    let heightChange = (diffNumLines * (composeTextView.font?.lineHeight)!)
    print("heightChange: \(heightChange)")

    if diffNumLines > 1 {
        if composeTextView.frame.size.height < (5 * (composeTextView.font?.lineHeight)!) {
    composeTextView.frame.size.height += heightChange
    composeViewHeightConstraint.constant += heightChange
    }
    }

    if diffNumLines < -1 {

// the number 1.975216273089 is the first value of newNumLines
        if newNumLines > 1.975216273089 && composeTextView.frame.size.height < (5 * (composeTextView.font?.lineHeight)!) {
            composeTextView.frame.size.height += heightChange
            composeViewHeightConstraint.constant += heightChange
        }
    }

    if composeTextView.frame.size.height >= (5 * (composeTextView.font?.lineHeight)!) && composeTextView.frame.size.height < (6 * (composeTextView.font?.lineHeight)!) && diffNumLines < -1 {

        composeTextView.frame.size.height += heightChange
        composeViewHeightConstraint.constant += heightChange
    }

    self.oldNumLines = newNumLines
}

EDIT 2:
I figured out how to fix the final issues. I was checking to see if the difference of the old and new number of lines (diffNumLines) was greater than 1 or less than -1. I didn't realize that sometimes diffNumLines is about .95 or -.95. There are instances when diffNumLines is around .44 and I didn't want to include that so I changed all the instances where I compared diffNumLines value to 1 or -1 so it was instead compared to .75 or -.75. I then added a few lines where I check if heightChange is greater than the height of 5 lines or less than the negative height of 5 lines. The height of 5 lines is not actually found by using the number 5, I had to get the actual height of 5 lines. I found with the debugging console and some print statements that if I type one word at a time, composeTextView stops increasing at a height of 100. It started at a height of 33 so simple subtraction tells us that the most I want it to change in height is 67. I set a couple rules so if heightChange > 67, heightChange will be set back down to 67 and if heightChange < -67, it will be set to -67. This ended up fixing everything so it was smooth and showed no bugs.

I only have one last concern that does not really need addressing but could prove useful. If I try to animate the change in height, it shows composeTextView pop up to its new y-location and then animates the bottom pushing down to fill its height. This looks really ugly so I decided against animation. What I would hope for is that composeView's bottom would stay where it is and it's top would animate up to fill its height. I think this would require altering its top constraint rather than its height though and I would rather not do this all over again.

I'm happy with how it is but there's still room for improvement.

Here's my final code:

@IBOutlet weak var composeTextItem: UIBarButtonItem!
@IBOutlet weak var composeUIView: UIView!
var oldNumLines = CGFloat()

override func viewDidLoad() {
    super.viewDidLoad()

    composeTextView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
    // the number 1.975216273089 is the first value of newNumLines
    self.oldNumLines = 1.97521627308861

}

deinit {
    composeTextView.removeObserver(self, forKeyPath: "contentSize")
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

    let newNumLines = composeTextView.contentSize.height / (composeTextView.font?.lineHeight)!

    let diffNumLines = newNumLines - oldNumLines

    var heightChange = (diffNumLines * (composeTextView.font?.lineHeight)!)

    if heightChange > 67 {
        heightChange = 67
        }
    if heightChange < -67 {
        heightChange = -67
    }

    if diffNumLines > 0.75 {
        if composeTextView.frame.size.height < (5 * (composeTextView.font?.lineHeight)!) {
    composeTextView.frame.size.height += heightChange
    composeViewHeightConstraint.constant += heightChange
    }
    }

    if composeTextView.frame.size.height > 33 {
    if diffNumLines < -0.75 {
        // the number 1.975216273089 is the first value of newNumLines
        if newNumLines > 1.97521627308861 && composeTextView.frame.size.height < (5 * (composeTextView.font?.lineHeight)!) {
            composeTextView.frame.size.height += heightChange
            composeViewHeightConstraint.constant += heightChange
        }
    }

    if composeTextView.frame.size.height >= (5 * (composeTextView.font?.lineHeight)!) && composeTextView.frame.size.height < (6 * (composeTextView.font?.lineHeight)!) && diffNumLines < -0.75 {

        composeTextView.frame.size.height += heightChange
        composeViewHeightConstraint.constant += heightChange
    }
    }

    self.oldNumLines = newNumLines
}
badman
  • 95
  • 1
  • 8

1 Answers1

2

I'm not sure why you're trying to change the bottom constraint of the view holding the textview... If you're changing the height of composeTextView, then you should also change the height of composeView by the same amount.

Also, instead of using textViewDidChange, I would add an observer for contentSize to the textview, which would then call a function whenever the content size changes. It makes your life a lot easier - something like this if you're using Swift 4:

composeTextView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)

Remember to call composeTextView.removeObserver(self, forKeyPath: "contentSize") in the deinit.

Then, override the observeValue function:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    // Change height of composeTextView and composeView here
}

A good way to accommodate for the change in height is to find the difference between the old number of lines vs. the new number of lines, since you know that every time this function is called, the number of lines are different. To get the number of lines, you could do something (Swift 4) like:

let numLines = composeTextView.contentSize.height / (composeTextView.font?.lineHeight)!

Calculate the difference of lines, multiply that by composeTextView.font?.lineHeight, and then that's the height you change everything by.

Hope this helps.

Edit

If you're setting your constraints correctly, you shouldn't need to move up your composeView, it'll be more work than needed. When the height of the composeView changes, the bottom shouldn't be moving - only the top should. When you show the keyboard, I would suggest changing the composeView position then, and only then. When you change the height and start typing multiple lines, I would suggest only changing height when needed.

Michael Hsu
  • 950
  • 1
  • 9
  • 25
  • 1
    Thanks Michael this was really helpful. I think I'm just tired so I wasn't thinking straight, of course I should be changing the heights of both. I took what you said and modified it to work for my code. I added what I did to the main post, if you see any room for improvement I would appreciate the input. – badman Mar 24 '18 at 06:25
  • Actually I just realized this doesn't work consistently. If I hold down the backspace button and it starts deleting words at a time, it will end up with a smaller than normal TextView at the end. Also, if I paste a chunk of text or delete a chunk of text it doesn't respond properly. – badman Mar 24 '18 at 06:50
  • Nevermind I fixed it. My edits are in the main description. – badman Mar 24 '18 at 08:31
  • sweet, glad I can help! – Michael Hsu Mar 24 '18 at 19:41