5

I am writing from scratch growing UITextView in my swift app. I put a textView on the view like this:

enter image description here

it is right above the keyboard.

The textView has constraints attached to the view: leading, bottom, top and trailing, all equals = 4.

The view has the following constraints:

trailing, leading, bottom, top and height

Height is an outlet in my code. I'm checking how many lines are in the textView and based on that I'm modifying height:

func textViewDidChange(textView: UITextView) { //Handle the text changes here
    switch(textView.numberOfLines()) {
    case 1:
        heightConstraint.constant = 38
        break
    case 2:
        heightConstraint.constant = 50
        break
    case 3:
        heightConstraint.constant = 70
        break
    case 4:
        heightConstraint.constant = 90
        break
    default:
        heightConstraint.constant = 90
        break
    }
}

The number of lines above is calculated by this extension:

extension UITextView{

  func numberOfLines() -> Int{
      if let fontUnwrapped = self.font{
          return Int(self.contentSize.height / fontUnwrapped.lineHeight)
      }
      return 0
  }
}

The initial height of the textView is 38. The initial font size in the textView is 15.

Now, it works nice, when user starts typing new line, but the textView is not set within full bounds of the view. I mean by that the fact, that it looks like this:

enter image description here

and it should look like this:

enter image description here

Why there is this extra white space being added and how can I get rid of it?

Currently when new line appears there's this white space, but when user scrolls the textView to center the text and get rid of the white space - it is gone forever, user is not able to scroll it up again so the white line is there. So for me it looks like some problem with refreshing content, but maybe you know better - can you give me some hints?

vadian
  • 274,689
  • 30
  • 353
  • 361
user3766930
  • 5,629
  • 10
  • 51
  • 104
  • i don't know exactly, by my guess changing **ScrollingEnabled to NO** may work.. – Gokul G Oct 07 '16 at 10:35
  • hmm @Gokul maybe it would help, but I need the `scrollingEnabled` to `true` because if there're more than 4 lines of text then the `textView` does no longer grow and user can scroll the content inside it... – user3766930 Oct 07 '16 at 11:55
  • did you build app for chat ? – Himanshu Moradiya Oct 08 '16 at 07:08
  • No, otherwise I'm pretty sure there are some already built-in components for that... I created this field for adding comments – user3766930 Oct 08 '16 at 16:52
  • @user3766930 Have you confirmed that the number of lines is correct? Meaning, if you are typing on a 3rd line in the textbox, is your `numberOfLines()` method returning 3. – Ian Moses Oct 08 '16 at 23:58
  • @IanMoses yes I did, unfortunatelly that's not the case here, the method works fine... – user3766930 Oct 09 '16 at 01:35
  • I have code that does this same thing. It works a little differently but may help if you have interest can put up the methods I use in mine. – Ian Moses Oct 09 '16 at 01:37
  • @IanMoses thanks man, I will appreciate that :) – user3766930 Oct 09 '16 at 19:07
  • Hi, I just want to make sure that I understood your problem: what is your problem [looks like](https://s11.postimg.org/uzv0y92xv/image.gif) and want do you [want to do](https://s21.postimg.org/e6pr2kq1j/image.gif). Is that it? :) – Ahmad F Oct 11 '16 at 07:45
  • Accepted answer here :- http://stackoverflow.com/a/38632593/3752143 – Mitul Marsoniya Oct 11 '16 at 09:12

2 Answers2

3

Here is a bit different approach I use in the comment section of one of the apps I'm developing. This works very similar to Facebook Messenger iOS app's input field. Changed outlet names to match with the ones in your question.

//Height constraint outlet of view which contains textView.
@IBOutlet weak var heightConstraint: NSLayoutConstraint!
@IBOutlet weak var textView: UITextView!

//Maximum number of lines to grow textView before enabling scrolling.
let maxTextViewLines = 5
//Minimum height for textViewContainer (when there is no text etc.)
let minTextViewContainerHeight = 40

func textViewDidChange(textView: UITextView) {
    let textViewVerticalInset = textView.textContainerInset.bottom + textView.textContainerInset.top
    let maxHeight = ((textView.font?.lineHeight)! * maxTextViewLines) + textViewVerticalInset
    let sizeThatFits = textView.sizeThatFits(CGSizeMake(textView.frame.size.width, CGFloat.max))

    if sizeThatFits.height < minTextViewContainerHeight {
        heightConstraint.constant = minTextViewContainerHeight
        textView.scrollEnabled = false
    } else if sizeThatFits.height < maxHeight {
        heightConstraint.constant = sizeThatFits.height
        textView.scrollEnabled = false
    } else {
        heightConstraint.constant = maxHeight
        textView.scrollEnabled = true
    }
}

func textViewDidEndEditing(textView: UITextView) {
    textView.text = ""
    heightConstraint.constant = minTextViewContainerHeight
    textView.scrollEnabled = false
}
enoktate
  • 725
  • 6
  • 12
0

I'm using ASTextInputAccessoryView. It handles everything for you and is very easy to set up:

enter image description here

import ASTextInputAccessoryView

class ViewController: UIViewController {

    var iaView: ASResizeableInputAccessoryView!    
    var messageView = ASTextComponentView()

    override func viewDidLoad() {
        super.viewDidLoad()

        let photoComponent = UINib
            .init(nibName: "PhotosComponentView", bundle: nil)
            .instantiateWithOwner(self, options: nil)
            .first as! PhotosComponentView

        messageView = ASTextComponentView(frame: CGRect(x: 0, y: 0, width: screenSize.width , height: 44))
        messageView.backgroundColor = UIColor.uIColorFromHex(0x191919)
        messageView.defaultSendButton.addTarget(self, action: #selector(buttonAction), forControlEvents: .TouchUpInside)

        iaView = ASResizeableInputAccessoryView(components: [messageView, photoComponent])
        iaView.delegate = self        
    }
}

//MARK: Input Accessory View
extension ViewController {
    override var inputAccessoryView: UIView? {
        return iaView
    }

    // IMPORTANT Allows input view to stay visible
    override func canBecomeFirstResponder() -> Bool {
        return true
    }

    // Handle Rotation
    override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)

        coordinator.animateAlongsideTransition({ (context) in
            self.messageView.textView.layoutIfNeeded()
        }) { (context) in
            self.iaView.reloadHeight()
        }
    }
}

// MARK: ASResizeableInputAccessoryViewDelegate
extension ViewController: ASResizeableInputAccessoryViewDelegate {

    func updateInsets(bottom: CGFloat) {
        var contentInset = tableView.contentInset
        contentInset.bottom = bottom
        tableView.contentInset = contentInset
        tableView.scrollIndicatorInsets = contentInset
    }

    func inputAccessoryViewWillAnimateToHeight(view: ASResizeableInputAccessoryView, height: CGFloat, keyboardHeight: CGFloat) -> (() -> Void)? {

        return { [weak self] in
            self?.updateInsets(keyboardHeight)
            self?.tableView.scrollToBottomContent(false)
        }
    }

    func inputAccessoryViewKeyboardWillPresent(view: ASResizeableInputAccessoryView, height: CGFloat) -> (() -> Void)? {
        return { [weak self] in
            self?.updateInsets(height)
            self?.tableView.scrollToBottomContent(false)
        }
    }

    func inputAccessoryViewKeyboardWillDismiss(view: ASResizeableInputAccessoryView, notification: NSNotification) -> (() -> Void)? {
        return { [weak self] in
            self?.updateInsets(view.frame.size.height)
        }
    }

    func inputAccessoryViewKeyboardDidChangeHeight(view: ASResizeableInputAccessoryView, height: CGFloat) {
        let shouldScroll = tableView.isScrolledToBottom
        updateInsets(height)
        if shouldScroll {
            self.tableView.scrollToBottomContent(false)
        }
    }
}

Now you just need to set up the actions for the buttons of the AccessoryView.

// MARK: Actions
extension ViewController {

    func buttonAction(sender: UIButton!) {

        // do whatever you like with the "send" button. for example post stuff to firebase or whatever
        // messageView.textView.text <- this is the String inside the textField

        messageView.textView.text = ""
    }

    @IBAction func dismissKeyboard(sender: AnyObject) {
        self.messageView.textView.resignFirstResponder()
    }

    func addCameraButton() {

        let cameraButton = UIButton(type: .Custom)
        let image = UIImage(named: "camera")?.imageWithRenderingMode(.AlwaysTemplate)
        cameraButton.setImage(image, forState: .Normal)
        cameraButton.tintColor = UIColor.grayColor()

        messageView.leftButton = cameraButton

        let width = NSLayoutConstraint(
            item: cameraButton,
            attribute: .Width,
            relatedBy: .Equal,
            toItem: nil,
            attribute: .NotAnAttribute,
            multiplier: 1,
            constant: 40
        )
        cameraButton.superview?.addConstraint(width)

        cameraButton.addTarget(self, action: #selector(self.showPictures), forControlEvents: .TouchUpInside)
    }

    func showPictures() {

        PHPhotoLibrary.requestAuthorization { (status) in
            NSOperationQueue.mainQueue().addOperationWithBlock({
                if let photoComponent = self.iaView.components[1] as? PhotosComponentView {
                    self.iaView.selectedComponent = photoComponent
                    photoComponent.getPhotoLibrary()
                }
            })
        }
    }
}
David Seek
  • 16,783
  • 19
  • 105
  • 136
  • 1
    downvote at least cares to comment? i swear if that was a "tactical downvote" by a concurrent answer, karma will destroy you – David Seek Oct 13 '16 at 11:55