Expand UITextView upwards from keyboard

Hi. I am trying to figure out how to do a simple thing, but all research that I have done so far has not clarified what the proper way to do it is. I am adding a UITextView that should expand while I am typing upwards and then enable scrolling when the maximum height is reached. Instead, my UITextView is expanding downwards. My code:


overridefunc didMove(to view: SKView)
{
txtMessage = UITextView()
        txtMessage.font = UIFont.systemFont(ofSize: 16)
        txtMessage.layer.cornerRadius = 5
        txtMessage.autocorrectionType = UITextAutocorrectionType.no
        txtMessage.keyboardType = UIKeyboardType.default
        txtMessage.returnKeyType = UIReturnKeyType.done
        txtMessage.isScrollEnabled = false
        txtMessage.delegate = self
        self.view!.addSubview(txtMessage)
        registerForKeyboardNotifications()
    }

func textViewDidChange(_ textView: UITextView)
    {
        let fixedWidth = textView.frame.size.width
      
        // Changing height of the message UITextView
        let newSize = textView.sizeThatFits(CGSize.init(width: fixedWidth, height: CGFloat(MAXFLOAT)))
        var newFrame = textView.frame
        newFrame.size = CGSize.init(width: CGFloat(fmaxf(Float(newSize.width), Float(fixedWidth))), height: newSize.height)
        txtMessage.frame = CGRect(origin: textView.frame.origin, size: CGSize(width: newFrame.width, height: newFrame.height))
   }

func registerForKeyboardNotifications()
    {
        let notificationCenter = NotificationCenter.default

        notificationCenter.addObserver( self,
                                        selector: #selector(GameScene.keyboardWillShow(_:)),
                                        name: UIResponder.keyboardWillShowNotification,
                                        object: nil )
       
        notificationCenter.addObserver( self,
                                        selector: #selector(GameScene.keyboardWillBeHidden(_:)),
                                        name: UIResponder.keyboardWillHideNotification,
                                        object: nil)
    }
   
    func unregisterForKeyboardNotifications()
    {
        let notificationCenter = NotificationCenter.default
        notificationCenter.removeObserver(  self,
                                            name: UIResponder.keyboardWillShowNotification,
                                            object: nil)
       
        notificationCenter.removeObserver(  self,
                                            name: UIResponder.keyboardWillHideNotification,
                                            object: nil)
    }
   
    @objc func keyboardWillShow(_ notification: Notification)
    {

        let txtMessageViewHeight : CGFloat = 41
       
        if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
        {
            let duration:TimeInterval = (notification.userInfo![UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
            let animationCurveRawNSN = notification.userInfo![UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber
            let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIView.AnimationOptions.curveEaseInOut.rawValue
            let animationCurve:UIView.AnimationOptions = UIView.AnimationOptions(rawValue: animationCurveRaw)
           
            keyboardHeight = keyboardFrame.cgRectValue.height
            let viewHeight = keyboardHeight + txtMessageViewHeight
           
            self.txtMessage.frame.origin.y -= viewHeight
            self.txtMessage.layoutIfNeeded()
        }
    }


Thank you!

Accepted Reply

I am really surprised.

Line 6, why do you use

txtMessage.frame

and not textView.frame ?



Could you also draw the border to see what happens and add some print:


func textViewDidChange(_ textView: UITextView)  {
        let fixedWidth = textView.frame.size.width
   
        // Changing height of the message UITextView
        let newSize = textView.sizeThatFits(CGSize.init(width: fixedWidth, height: CGFloat(MAXFLOAT)))
        var newFrame = textView.frame
        print("textView.frame", textView.frame)

        newFrame.size = CGSize.init(width: CGFloat(fmaxf(Float(newSize.width), Float(fixedWidth))), height: newSize.height)
        var newOrigin = textView.frame.origin
        newOrigin.y -= (newSize.height - newFrame.size.height)

        textView.frame = CGRect(origin: textView.frame.origin, size: CGSize(width: newFrame.width, height: newFrame.height))
        print("New textView.frame", textView.frame)
        txtMessage.layer.borderColor = UIColor.red.cgColor
        txtMessage.layer.borderWidth = 1.0

   }

Replies

I read rapidly, but I think you were mislead by the inverse y coordinate issue. The origin is the top of TextView.


If you do this:

        let label1 = UITextView(frame: CGRect(x: 5, y: 80, width: 80, height: 20))
        label1.text = "Label 1\nsuite"
        label1.layer.borderColor = UIColor.black.cgColor
        label1.layer.borderWidth = 1.0
        let label2 = UITextView(frame: CGRect(x: 85, y: 80, width: 80, height: 40))
        label2.text = "Label 2\nsuite"
        label2.layer.borderColor = UIColor.black.cgColor
        label2.layer.borderWidth = 1.0
        self.view.addSubview(label1)
        self.view.addSubview(label2)

you get 2 UITextViews that are aligned at top.


So, Change into

let newSize = textView.sizeThatFits(CGSize.init(width: fixedWidth, height: CGFloat(MAXFLOAT))) 
 var newFrame = textView.frame
newFrame.size = CGSize.init(width: CGFloat(fmaxf(Float(newSize.width), Float(fixedWidth))), height: newSize.height)
var newOrigin = textView.frame.origin
newOrigin.y -= (newSize.height - newFrame.size.height)
txtMessage.frame = CGRect(origin: newOrigin, size: CGSize(width: newFrame.width, height: newFrame.height))

Thanks, Claude31, but nope. This does not address the issue. Exactly the same behavior. Cheers.

I am really surprised.

Line 6, why do you use

txtMessage.frame

and not textView.frame ?



Could you also draw the border to see what happens and add some print:


func textViewDidChange(_ textView: UITextView)  {
        let fixedWidth = textView.frame.size.width
   
        // Changing height of the message UITextView
        let newSize = textView.sizeThatFits(CGSize.init(width: fixedWidth, height: CGFloat(MAXFLOAT)))
        var newFrame = textView.frame
        print("textView.frame", textView.frame)

        newFrame.size = CGSize.init(width: CGFloat(fmaxf(Float(newSize.width), Float(fixedWidth))), height: newSize.height)
        var newOrigin = textView.frame.origin
        newOrigin.y -= (newSize.height - newFrame.size.height)

        textView.frame = CGRect(origin: textView.frame.origin, size: CGSize(width: newFrame.width, height: newFrame.height))
        print("New textView.frame", textView.frame)
        txtMessage.layer.borderColor = UIColor.red.cgColor
        txtMessage.layer.borderWidth = 1.0

   }

Hello.


I think, I know why I had issues with my code. Looks like the textViewDidChange method was also influenced by didChangeSize(oldSize:) method which was also called every time I typed in a character and which I needed to handle rotation of my device.


Anyways, I think I have come up with a solution how to handle expanding a textView which also gets adjusted with a rotation. Here is the code for the peer review.


import UIKit

class ViewController: UIViewController, UITextViewDelegate
{
   
    var textView: UITextView!
    var btn: UIButton!
    var btnSend: UIButton!
    var keyboardHeight : CGFloat = 0
    var isKeyboardShown = false
    var textViewHeight : CGFloat = 0
    var textView_inset : CGFloat = 10
    var textView_insetBottom : CGFloat = 5
   
    let btnSendWidth : CGFloat = 50
    let btnSendHeight : CGFloat = 50

   
   
    override func viewDidLoad()
    {
        super.viewDidLoad()
       
        textViewHeight = 35

       
        textView = UITextView(frame: CGRect(x: textView_inset+self.view.safeAreaInsets.left,
                                            y: self.view!.bounds.height,
                                            width: self.view.frame.width-textView_inset*3-self.view.safeAreaInsets.left-self.view.safeAreaInsets.right-btnSendWidth,
                                            height: textViewHeight))
       
        textView.textAlignment = NSTextAlignment.justified
        textView.backgroundColor = UIColor.lightGray
       
        // Use RGB colour
        textView.backgroundColor = UIColor(red: 39/255, green: 53/255, blue: 182/255, alpha: 1)
       
        // Update UITextView font size and colour
        textView.font = UIFont(name: "Verdana", size: 17)
        textView.textColor = UIColor.white

       
        // Make UITextView web links clickable
        textView.isSelectable = true
        textView.isEditable = true
        textView.dataDetectorTypes = UIDataDetectorTypes.link
        textView.isScrollEnabled = false
       
       
        // Make UITextView corners rounded
        textView.layer.cornerRadius = 10
       
        // Enable auto-correction and Spellcheck
        textView.autocorrectionType = UITextAutocorrectionType.yes
        textView.spellCheckingType = UITextSpellCheckingType.yes
        // myTextView.autocapitalizationType = UITextAutocapitalizationType.None
       
       
        self.view.addSubview(textView)
        textView.delegate = self
       
        registerForKeyboardNotifications()
       
       
        btn = UIButton()
        btn.setTitle("PRESS", for: .normal)
        btn.setTitleColor(.red, for: .normal)
        btn.frame = CGRect(x: 50, y: 150, width: 100, height: 50)
        btn.addTarget(self, action: #selector(self.showTextView(sender:)), for: .touchUpInside)
        self.view.addSubview(btn)
       
       
       
        btnSend = UIButton()
        btnSend.setTitle("SEND", for: .normal)
        btnSend.setTitleColor(.white, for: .normal)
        btnSend.backgroundColor = .red
        btnSend.frame = CGRect(x: self.view.frame.width-self.view.safeAreaInsets.right-btnSendWidth-textView_inset,
                               y: self.view!.bounds.height,
                               width: btnSendWidth,
                               height: btnSendHeight)
        btnSend.addTarget(self, action: #selector(self.sendMessage(sender:)), for: .touchUpInside)
        self.view.addSubview(btnSend)

    }

   
    override func viewWillLayoutSubviews()
    {
        var textView_y : CGFloat = 0
        var btnSend_y : CGFloat = 0
       
        // Required to ensure that the TextView's height is changed dynamically!
        if isKeyboardShown
        {
            textView_y = self.view!.bounds.height-keyboardHeight-textView.frame.height-textView_insetBottom
            btnSend_y = self.view!.bounds.height-keyboardHeight-btnSendHeight-textView_insetBottom
        }
        else
        {
            textView_y = self.view!.bounds.height
            btnSend_y = self.view!.bounds.height
        }
       
        textView.frame = CGRect(x: textView_inset+self.view.safeAreaInsets.left,
                                y: textView_y,
                                width: self.view.frame.width-textView_inset*3-self.view.safeAreaInsets.left-self.view.safeAreaInsets.right-btnSendWidth,
                                height: textViewHeight)
       
        btnSend.frame = CGRect(x: self.view.frame.width-self.view.safeAreaInsets.right-self.btnSendWidth-textView_inset,
                               y: btnSend_y,
                               width: btnSendWidth,
                               height: btnSendHeight)
    }
   
   
    @objc func showTextView(sender: UIButton!)
    {
        textView.becomeFirstResponder()
    }
   
   
    @objc func sendMessage(sender: UIButton!)
    {
        print("------SEND MESSAGE-------")
    }
   

    func textViewDidChange(_ textView: UITextView)
    {
        let fixedWidth = textView.frame.size.width
             
        // Changing height of the message UITextView
        var newSize = textView.sizeThatFits(CGSize.init(width: fixedWidth, height: CGFloat(MAXFLOAT)))
       
       
        // Limit the height to 100
        if newSize.height > 100
        {
            newSize.height = 100
            textView.isScrollEnabled = true
        }
        else
        {
            textView.isScrollEnabled = false
        }
       
       
        var newFrame = textView.frame
        newFrame.size = CGSize.init(width: CGFloat(fmaxf(Float(newSize.width), Float(fixedWidth))), height: newSize.height)
       
       
        textViewHeight = newFrame.height
       
        var newOrigin = textView.frame.origin
       
        newOrigin.y -= (newSize.height - newFrame.size.height)
          
        textView.frame = CGRect(origin: textView.frame.origin, size: CGSize(width: newFrame.width, height: newFrame.height))
    }

      
    func registerForKeyboardNotifications()
    {
        let notificationCenter = NotificationCenter.default
          
        notificationCenter.addObserver( self,
                                        selector: #selector(ViewController.keyboardWillShow(_:)),
                                        name: UIResponder.keyboardWillShowNotification,
                                        object: nil )
          
        notificationCenter.addObserver( self,
                                        selector: #selector(ViewController.keyboardWillBeHidden(_:)),
                                        name: UIResponder.keyboardWillHideNotification,
                                        object: nil)
    }
      

    func unregisterForKeyboardNotifications()
    {
        let notificationCenter = NotificationCenter.default
        notificationCenter.removeObserver(  self,
                                            name: UIResponder.keyboardWillShowNotification,
                                            object: nil)
          
        notificationCenter.removeObserver(  self,
                                            name: UIResponder.keyboardWillHideNotification,
                                            object: nil)
    }
      

    @objc func keyboardWillShow(_ notification: Notification)
    {
        isKeyboardShown = true
       
        // Reset the position of the textview to the bottom of the screen
        textView.frame = CGRect(x: textView_inset+self.view.safeAreaInsets.left,
                                y: self.view!.bounds.height,
                                width: self.view.frame.width-textView_inset*3-self.view.safeAreaInsets.left-self.view.safeAreaInsets.right-btnSendWidth,
                                height: textViewHeight)
       
        btnSend.frame = CGRect(x: self.view.frame.width-self.view.safeAreaInsets.right-btnSendWidth-textView_inset,
                               y: self.view!.bounds.height,
                               width: btnSendWidth,
                               height: btnSendHeight)
       
       
        if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
        {
            keyboardHeight = keyboardFrame.cgRectValue.height
           
            // Place the TextView above the keyboard
            self.textView.frame.origin.y -= (keyboardHeight+self.textView.frame.height+textView_insetBottom)
           
            // Determine a new TextView height dynamically
            let fixedWidth = textView.frame.size.width // Width never changes
            var newSize = textView.sizeThatFits(CGSize.init(width: fixedWidth, height: CGFloat(MAXFLOAT))) // Height is based on the number of characters
           
           
            // Limit the height to 100
            if newSize.height > 100
            {
                newSize.height = 100
                textView.isScrollEnabled = true
            }
            else
            {
                textView.isScrollEnabled = false
            }
           
           

            var newFrame = textView.frame // A new frame: Fixed width; Dynamically changed height
            newFrame.size = CGSize.init(width: CGFloat(fmaxf(Float(newSize.width), Float(fixedWidth))), height: newSize.height)
            textViewHeight = newFrame.height // Assign the height value to the object property
           
            textView.frame = CGRect(origin: textView.frame.origin, size: CGSize(width: newFrame.width, height: newFrame.height))
           
            self.textView.layoutIfNeeded()
           
           
            self.btnSend.frame.origin.y -= (keyboardHeight+self.btnSend.frame.height+textView_insetBottom)
            btnSend.frame = CGRect(origin: btnSend.frame.origin, size: CGSize(width: btnSendWidth, height: btnSendHeight))
        }
    }


    @objc func keyboardWillBeHidden(_ notification : NSNotification)
    {
        isKeyboardShown = false
       
        if let _: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
        {
              
            //let newHeight: CGFloat
            let duration:TimeInterval = (notification.userInfo![UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
            let animationCurveRawNSN = notification.userInfo![UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber
            let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIView.AnimationOptions.curveEaseInOut.rawValue
            let animationCurve:UIView.AnimationOptions = UIView.AnimationOptions(rawValue: animationCurveRaw)
              
            keyboardHeight = 0

            UIView.animate(withDuration: duration,
                           delay: TimeInterval(0),
                           options: animationCurve,
                           animations: {
                            self.textView.frame.origin.y = self.view!.bounds.height
                            self.textView.layoutIfNeeded()
                            self.btnSend.frame.origin.y = self.view!.bounds.height
                            },
                           completion: nil)
        }
          
    }


    func textViewDidBeginEditing(_ textView: UITextView)
    {
        textView.becomeFirstResponder()
       
        let tapRecognizer = UITapGestureRecognizer(target: self,
                                                      action: #selector(tapDetected(_:)))
          
        if self.view != nil {self.view!.addGestureRecognizer(tapRecognizer)}
    }
      
      

    func textViewDidEndEditing(_ textView: UITextView)
    {
        // Make the active field nil to hide the keyboard
        //self.activeField = nil;
    }
      

    @objc func tapDetected(_ tapRecognizer: UITapGestureRecognizer)
    {
        textView?.resignFirstResponder()
        if self.view != nil {self.view!.removeGestureRecognizer(tapRecognizer)}
    }

}


Thanks!

In your code that works:


   func textViewDidChange(_ textView: UITextView) 
    { 
        let fixedWidth = textView.frame.size.width 
              
        // Changing height of the message UITextView 
        var newSize = textView.sizeThatFits(CGSize.init(width: fixedWidth, height: CGFloat(MAXFLOAT))) 
        
        
        // Limit the height to 100 
        if newSize.height > 100 
        { 
            newSize.height = 100 
            textView.isScrollEnabled = true 
        } 
        else 
        { 
            textView.isScrollEnabled = false 
        } 
        
        var newFrame = textView.frame 
        newFrame.size = CGSize.init(width: CGFloat(fmaxf(Float(newSize.width), Float(fixedWidth))), height: newSize.height) 
        
        
        textViewHeight = newFrame.height 
        
        var newOrigin = textView.frame.origin 
        newOrigin.y -= (newSize.height - newFrame.size.height) 
           
        textView.frame = CGRect(origin: textView.frame.origin, size: CGSize(width: newFrame.width, height: newFrame.height)) 
    }

It seems that newOrigin is never used.


Why don't you use line 29 ?

Good observation, Claude31.


Replaced with:


textView.frame = CGRect(origin: newOrigin, size: CGSize(width: newFrame.width, height: newFrame.height))


If you notice any other issues, this would be greatly appreciated!


Thanks a lot!


Igor