24

In my view controller, I have an UITextView. This textview is filled with a string. The string can be short or long. Depending on the length of the string, the height of the textview has to adjust. I use storyboards and auto layout.

I have some troubles with the height of the textview.

Sometimes, the height is perfectly adjusted to the text. Sometimes, no, the text is cropped on the first line. Below, the textview is yellow. the blue is a containerview inside a scrollview. Purple is the place for a picture.

title1 title2

The texts are from my core data base, they are string attributes. Between the screen 1 and 2, the only thing changed is the string.
If I print the strings in the console I have the correct texts :

my amazing new title

Another funny title for demo

The constraints of my textview :

enter image description here

I don't understand why I have 2 different displays.

EDIT

I tried the @Nate advice, in viewDidLoad, I added:

    myTextViewTitle.text="my amazing new title"

    myTextViewTitle.setContentHuggingPriority(1000, forAxis: UILayoutConstraintAxis.Vertical)
    myTextViewTitle.setContentCompressionResistancePriority(1000, forAxis: UILayoutConstraintAxis.Vertical)
    myTextViewTitle.setTranslatesAutoresizingMaskIntoConstraints(false)  

    let views = ["new_view": myTextViewTitle]
    var constrs = NSLayoutConstraint.constraintsWithVisualFormat("|-8-[new_view]-8-|", options: NSLayoutFormatOptions(0), metrics: nil, views: views)
    self.view.addConstraints(constrs)
    self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-8-[new_view]", options: NSLayoutFormatOptions(0), metrics: nil, views: views))
    self.myTextViewTitle.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[new_view(220@300)]", options: NSLayoutFormatOptions(0), metrics: nil, views: views))

No results. With or without height contraint added for my textview in interface builder...

EDIT 2

I need an UITextView and not an UIlabel, because of the back button, to use an exclusion path.

     let exclusionPathBack:UIBezierPath = UIBezierPath(rect: CGRect(x:backButton.bounds.origin.x, y:backButton.bounds.origin.y, width:backButton.bounds.width+10, height:backButton.bounds.height))
     myTextViewTitle.textContainer.exclusionPaths=[exclusionPathBack]
cmii
  • 3,556
  • 8
  • 38
  • 69
  • Try the solution listed here: http://stackoverflow.com/questions/16009405/uilabel-sizetofit-doesnt-work-with-autolayout-ios6. The method `sizeToFit` is the way to go but in your case you need constraints as well. – Nate Lee Apr 03 '15 at 12:36
  • Is scroll enabled in UITextView? – Abdullah Apr 03 '15 at 15:43
  • @NateLee, this solution is for an UILabel. I use a UITextView. – cmii Apr 03 '15 at 16:26
  • @abdullah scroll isn't enabled in UITextView – cmii Apr 03 '15 at 16:27
  • sizeToFit is an UIView method, and UILabel and UITextView both subclass UIView. The solution applies to you as well. – Nate Lee Apr 05 '15 at 09:11
  • The screenshots you posted match the constraints you posted. Can you post a screenshot showing the problem? – Aaron Brager Apr 05 '15 at 22:02
  • @AaronBrager look at the left screenshot, the UITextView is on 1 line, it should be on 2 lines. The edit posted changes nothing. – cmii Apr 06 '15 at 09:10
  • It looks like the issue is caused by whatever you're doing to indent the text to avoid the collision with the black circle. How are you doing that? – Aaron Brager Apr 06 '15 at 13:31
  • @AaronBrager I need an UITextView and not an UIlabel, because of the back button, to use an exclusion path. I copied my code in the post (edit 2) – cmii Apr 06 '15 at 15:07

7 Answers7

41

I found a solution.

I was focus on the height of my UITextView, then I read a post talking about the aspect ratio. I wanted to try and that worked!

So in the storyboard, I added an aspect ratio for the textview, and I checked "Remove at build time" option.

In viewDidLayoutSubviews(), I wrote :

override func viewDidLayoutSubviews() {

    super.viewDidLayoutSubviews()

    let contentSize = self.myTextViewTitle.sizeThatFits(self.myTextViewTitle.bounds.size)
    var frame = self.myTextViewTitle.frame
    frame.size.height = contentSize.height
    self.myTextViewTitle.frame = frame

    aspectRatioTextViewConstraint = NSLayoutConstraint(item: self.myTextViewTitle, attribute: .Height, relatedBy: .Equal, toItem: self.myTextViewTitle, attribute: .Width, multiplier: myTextViewTitle.bounds.height/myTextViewTitle.bounds.width, constant: 1)
    self.myTextViewTitle.addConstraint(aspectRatioTextViewConstraint!)

}

It works perfectly.

cmii
  • 3,556
  • 8
  • 38
  • 69
  • This is a good solution, I am using `textView.sizeThatFits` in the textViewDidChange delegate method, works perfectly. Good answer, thanks – Swinny89 Sep 04 '15 at 15:59
  • I tried lots of different solutions and external libraries for this problem and this is the only one that worked for me. Thank you good sir. – Ces Jun 04 '16 at 01:22
  • You Saved my Day too @cmii Thanks – Raghav Chopra Sep 20 '17 at 15:12
23

First, disable UITextView's scrollable. Two options:

  1. uncheck Scrolling Enabled in .xib.
  2. [TextView setScrollEnabled:NO];

Create a UITextView and connect it with IBOutlet (TextView). Add a UITextView height constraint with default height, connect it with IBOutlet (TextViewHeightConstraint). When you set your UITextView’s text asynchronously you should calculate the height of UITextView and set UITextView’s height constraint to it. Sample code:

CGSize sizeThatFitsTextView = [TextView sizeThatFits:CGSizeMake(TextView.frame.size.width, MAXFLOAT)];

TextViewHeightConstraint.constant = sizeThatFitsTextView.height; 
Serge Bilyk
  • 875
  • 9
  • 10
  • 3
    It should be noted that if you're using auto-layout, the "Scrolling Enabled" selecting in the storyboard affects how the constraints are laid out, even though it doesn't show that relationship explicitly. I struggled with autolayout constraint garbage until I un-ticked the checkbox. Auto-layout resolved itself perfectly after that. – dst3p Dec 19 '16 at 17:37
  • @dstepan Thanks for that trick with setting scrolling enabled to false!!! It really helped – Valeriy Mar 19 '17 at 13:24
8

In Swift 3.0, where tvHeightConstraint is the height constraint of the subject TextView (if using for multiple textviews, would add it to the function inputs):

func textViewDidChange(textView: UITextView) {

        let size = textView.sizeThatFits(CGSize(width: textView.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
        if size.height != tvHeightConstraint.constant && size.height > textView.frame.size.height{
            tvHeightConstraint.constant = size.height
            textView.setContentOffset(CGPoint.zero, animated: false)
        }
    }

H/T to @cmi and @Pablo Ruan

agrippa
  • 819
  • 10
  • 8
5

In swift 2.3:

 func textViewDidChange(textView: UITextView) {

    let size = textView.sizeThatFits(CGSize(width: textView.frame.size.width, height: CGFloat.max))
    if size.height != TXV_Height.constant && size.height > textView.frame.size.height{
        TXV_Height.constant = size.height
        textView.setContentOffset(CGPointZero, animated: false)
    }
}
Pablo Ruan
  • 1,681
  • 1
  • 17
  • 12
0

Why not have an image for the back button and a UILabel for your text, and use autolayout to position them? And maybe a third view for the yellow background.

koen
  • 5,383
  • 7
  • 50
  • 89
  • Yes, there is a reason. It's because I would like have the 1st line at the left of the back button, and 2nd and 3th lines below the button. In my screenshot I haven't set the line height yet. – cmii Apr 06 '15 at 16:51
0

My simple solution to an extremely similar problem:

I used labels with the number of lines set to 0 instead of trying to use text fields or views. Then I clipped the leading and trailing edges to the labels' containers which constrain their widths in my case, with 0 as the constant for the constraints. The catch is to set the number of lines to 0 to allow the labels to grow or shrink according to their content. The text never gets chopped and the height of the labels never exceed their contents' for all of the different size classes I configured for the widths (the container widths in my case). Tested for IOS 8.0 and later (also make sure 'explicit' is unchecked next to 'Preferred Width' attributes for all lables for the label widths to adjust for different size classes) - works perfectly.

AshleyC
  • 11
  • 3
0

I have adapted your code and used it for both: layout subview and textViewDidChange. Thank you very much that made my day after an hour of troubleshooting...

override func viewDidLayoutSubviews() {

    super.viewDidLayoutSubviews()
    textViewDidChange(self.recordingTitleTV)


}

func textViewDidChange(_ textView: UITextView) {

    let contentSize = textView.sizeThatFits(textView.bounds.size)
    var frame = textView.frame
    frame.size.height = contentSize.height
    textView.frame = frame

    let aspectRatioTextViewConstraint = NSLayoutConstraint(item: textView, attribute: .height, relatedBy: .equal, toItem: textView, attribute: .width, multiplier: textView.bounds.height/textView.bounds.width, constant: 1)
    textView.addConstraint(aspectRatioTextViewConstraint)
}
AppNerd
  • 99
  • 6