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
}