38

I wanted to know when a text is wrapped by the frame of the text view is there any delimiter with which we can identify whether the text is wrapped or not.

For instance if my text view has a width of 50 px and text is exceeding that, it wraps the text to next line.

I wanted to count the number of lines in my text view. Now "\n" and "\r" are not helping me.

My code is:

NSCharacterSet *aCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"\n\r"];
    NSArray *myArray = [textViewText componentsSeparatedByCharactersInSet:aCharacterSet];
    NSLog(@"%d",[myArray count]);
Kundan
  • 3,084
  • 2
  • 28
  • 65
Abhinav
  • 37,684
  • 43
  • 191
  • 309

16 Answers16

38

You need to use the lineHeight property, and font lineHeight:

Objective-C

int numLines = txtview.contentSize.height / txtview.font.lineHeight;

Swift

let numLines = (txtview.contentSize.height / txtview.font.lineHeight) as? Int

I am getting correct number of lines.

halfer
  • 19,824
  • 17
  • 99
  • 186
Himanshu padia
  • 7,428
  • 1
  • 47
  • 45
  • 6
    This worked great for me, however, a point that is not mentioned in the answer is that it does not account for attributed text that has varying fonts. Specifically, fonts with varying line heights. For those cases, you would probably have to use [boundingRectWithSize:options:attributes:context:](https://developer.apple.com/documentation/foundation/nsstring/1524729-boundingrectwithsize) since [sizeWithFont:constrainedToSize:](https://developer.apple.com/documentation/foundation/nsstring/1619910-sizewithfont) is now deprecated. – strwils Nov 19 '18 at 17:00
  • Also, it doesn't work for the case when a text is edited, but `contentSize` remains the same. – olha May 16 '21 at 15:53
32

This variation takes into account how you wrap your lines and the max size of the UITextView, and may output a more precise height. For example, if the text doesn't fit it will truncate to the visible size, and if you wrap whole words (which is the default) it may result in more lines than if you do otherwise.

UIFont *font = [UIFont boldSystemFontOfSize:11.0];
CGSize size = [string sizeWithFont:font 
                      constrainedToSize:myUITextView.frame.size 
                      lineBreakMode:UILineBreakModeWordWrap]; // default mode
float numberOfLines = size.height / font.lineHeight;
luvieere
  • 37,065
  • 18
  • 127
  • 179
Jano
  • 62,815
  • 21
  • 164
  • 192
17

Swift extension:

Using @himanshu padia answer

//MARK: - UITextView
extension UITextView{

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

}

Usage : yourTextView.numberOfLines()

be aware that if for some reason the font of the text view is nil, the return will be zero.

Juan Boero
  • 6,281
  • 1
  • 44
  • 62
15

Swift 3:

let layoutManager:NSLayoutManager = textView.layoutManager
let numberOfGlyphs = layoutManager.numberOfGlyphs
var numberOfLines = 0
var index = 0
var lineRange:NSRange = NSRange()

while (index < numberOfGlyphs) {
    layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &lineRange)
    index = NSMaxRange(lineRange);
    numberOfLines = numberOfLines + 1
}

print(numberOfLines)
castillejoale
  • 519
  • 4
  • 13
12

Swift 4 version of Luke Chase's answer

let numberOfGlyphs = textView.layoutManager.numberOfGlyphs
var index = 0, numberOfLines = 0
var lineRange = NSRange(location: NSNotFound, length: 0)

while index < numberOfGlyphs {
  textView.layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &lineRange)
  index = NSMaxRange(lineRange)
  numberOfLines += 1
}
Yeesha
  • 131
  • 1
  • 4
11

I found the perfect solution to this problem in Apple's Text Layout Programming Guide. Here is the solution Apple provides:

NSLayoutManager *layoutManager = [textView layoutManager];
unsigned numberOfLines, index;
unsigned numberOfGlyphs = [layoutManager numberOfGlyphs];
NSRange lineRange;

for (numberOfLines = 0, index = 0; index < numberOfGlyphs; numberOfLines++){
    (void) [layoutManager lineFragmentRectForGlyphAtIndex:index effectiveRange:&lineRange];
    index = NSMaxRange(lineRange);
}

This could easily be written into an extension for UITextView, or as a standalone method taking in a UITextView object as a parameter

Luke Chase
  • 371
  • 4
  • 7
11

Swift 5.0

extension UITextView {
    func sizeFit(width: CGFloat) -> CGSize {
        let fixedWidth = width
        let newSize = sizeThatFits(CGSize(width: fixedWidth, height: .greatestFiniteMagnitude))
        return CGSize(width: fixedWidth, height: newSize.height)
    }

    func numberOfLine() -> Int {
        let size = self.sizeFit(width: self.bounds.width)
        let numLines = Int(size.height / (self.font?.lineHeight ?? 1.0))
        return numLines
    }
}
Tà Truhoada
  • 584
  • 7
  • 23
7

You need to consider textView.textContainerInset, also need to round the calculated value since line number definitely is an integer

float rawLineNumber = (textView.contentSize.height - textView.textContainerInset.top - textView.textContainerInset.bottom) / textView.font.lineHeight;
int finalLineNumber = round(rawLineNumber)

In real case, you may see following result rawLineNumber = 3.008099 finalLineNumber = 3 (3 lines)

LiangWang
  • 8,038
  • 8
  • 41
  • 54
7

Improved and update Luke Chase's answer to Swift 5, XCode 11, iOS 13 to get text view number of lines and autoresize table view cell height.

  1. You can use storyboard with static cell height to design it as you want. Make UITextView scroll enable: false (disable scroll).

  2. In viewDidLoad add your estimated row height and your textView delegate.

override func viewDidLoad() {
        super.viewDidLoad()

        quoteTextView.delegate = self
        tableView.estimatedRowHeight = 142

    }
  1. Add table view delegates for heightForRowAt:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return UITableView.automaticDimension
}
  1. Conform to UITextViewDelegate to listen when user inputs text.
extension ViewController: UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        // Refresh tableView cell
        if textView.numberOfLines > 2 { // textView in storyboard has two lines, so we match the design
            // Animated height update
            DispatchQueue.main.async {
                self.tableView?.beginUpdates()
                self.tableView?.endUpdates()
            }
        }
    }
}
  1. Add UITextView extension so you avoid redundant code and use all over the app.
extension UITextView {
    var numberOfLines: Int {
        // Get number of lines
        let numberOfGlyphs = self.layoutManager.numberOfGlyphs
        var index = 0, numberOfLines = 0
        var lineRange = NSRange(location: NSNotFound, length: 0)

        while index < numberOfGlyphs {
            self.layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &lineRange)
          index = NSMaxRange(lineRange)
          numberOfLines += 1
        }

        return numberOfLines
    }
}

-> Do not forgot to disable uitextview scroll. Cheers!<-

Preview

enter image description here

Egzon P.
  • 4,498
  • 3
  • 32
  • 31
5

Use this (where _text_v is your text view):

-(NSInteger) linesCount {
    return _text_v.contentSize.height/_text_v.font.lineHeight;
}
Max
  • 16,679
  • 4
  • 44
  • 57
  • 1
    Hi Max. I tries using it but for a data of 8 lines in my text view this methos is returning me a count of 5. Important thing is that I have some 7 \n characters in the data. – Abhinav Apr 29 '11 at 21:58
4

For those who have dynamic font sizes, or a mix or different font sizes for rich messaging. You can use NSLayoutManager to accurately calculate the line height as others have correctly pointed out.

An extra gotcha is a simple implementation doesn't take into account new lines as Dmitry Petukhov has pointed out. You can use the layout manager to work this out by checking layoutManager.extraLineFragmentRect as per the docs NSLayoutManager#extraLineFragmentRect

The rectangle that encloses the insertion point in the extra line fragment rectangle

The rectangle is defined in the coordinate system of its NSTextContainer. NSZeroRect if there is no extra line fragment rectangle.

So we just need to detect when extraLineFragmentUsedRect is populated (!= CGRect.zero) - which means there is a newline as the end, and correct the line count accordingly.

Full example below.

extension UITextView {
    public var lineCount: Int {
        let numberOfGlyphs = layoutManager.numberOfGlyphs
        var index = 0, numberOfLines = 0
        var lineRange = NSRange(location: NSNotFound, length: 0)

        while index < numberOfGlyphs {
            layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &lineRange)
            index = NSMaxRange(lineRange)
            numberOfLines += 1
        }
        // Take into account newlines at the bottom.
        if layoutManager.extraLineFragmentUsedRect != CGRect.zero {
            numberOfLines += 1
        }
        return numberOfLines
    }
}
revilo
  • 169
  • 1
  • 8
3
extension UITextView {
       func numberOfLines(textView: UITextView) -> Int {
        let layoutManager = textView.layoutManager
        let numberOfGlyphs = layoutManager.numberOfGlyphs
        var lineRange: NSRange = NSMakeRange(0, 1)
        var index = 0
        var numberOfLines = 0
        
        while index < numberOfGlyphs {
            layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &lineRange)
            index = NSMaxRange(lineRange)
            numberOfLines += 1
        }
        return numberOfLines
    }
}

Working Fine for me

Rahul Vyas
  • 28,260
  • 49
  • 182
  • 256
Softlabsindia
  • 791
  • 6
  • 11
  • 1
    Method numberOfLines() do not count new line then it have first \n symbol. This method is incorrect – Dmitry Petukhov Jul 16 '18 at 11:52
  • @DmitryPetukhov I know it's been a while, but added a solution which correctly takes into account newlines [here](https://stackoverflow.com/a/65577738/2594130) – revilo Jan 05 '21 at 11:12
3

I spent a lot of time trying to calculate in real time the number of lines for an input TextView (say a chat text entry box) and the only solution that works in such a case is Luke Chase's (for some reason the frame.height approach appears to update only ever 3rd or fourth letter typed into a textView and so is not accurate).

As a commenter mentioned however, there is a small bug in which a user generated line breaks ("\n" or keyboard return press) are not properly accounted for. Even stranger, it only "misses" the first such line-break, any subsequent ones are correctly captured (say you go to the line 4 times it will return only 3 lines clearly missing the first line break).

So to go around that bug I simple look record the first such line-break (character "\n") and manually add a line to the # of lines that the gliph method returns.

In code that gives:

    func offSetTableViewIfNeeded() {
    let numberOfGlyphs = textView.layoutManager.numberOfGlyphs
    var index : Int = 0
    var lineRange = NSRange(location: NSNotFound, length: 0)
    var currentNumOfLines : Int = 0
    var numberOfParagraphJump : Int = 0

    while index < numberOfGlyphs {
        textView.layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &lineRange)
        index = NSMaxRange(lineRange)
        currentNumOfLines += 1

        // Observing whether user went to line and if it's the first such line break, accounting for it.
        if textView.text.last == "\n", numberOfParagraphJump == 0 {
            numberOfParagraphJump = 1
        }
    }

    currentNumOfLines += numberOfParagraphJump

    print("Number of lines is:", currentNumOfLines)

Hope this helps others who've been struggling with the super weird behavior of input textView (can't understand why Apple does not provide a # of line method out of the box!).

Franc
  • 351
  • 1
  • 3
  • 8
  • Thanks for the idea, this solved my problem. Just a question, why don't you just add a new line after the loop if the last character was `\n`? Why perform this check with `numberOfParagraphJump`? – André Jul 29 '19 at 18:01
0

I think that you can try to use NSLayoutManager:

NSLayoutManager *layoutManager = [textView layoutManager];
unsigned numberOfLines, index, numberOfGlyphs =
        [layoutManager numberOfGlyphs];
NSRange lineRange;
for (numberOfLines = 0, index = 0; index < numberOfGlyphs; numberOfLines++){
    (void) [layoutManager lineFragmentRectForGlyphAtIndex:index
            effectiveRange:&lineRange];
    index = NSMaxRange(lineRange);
}

Ref

CountLines

MichaelMao
  • 528
  • 6
  • 15
0

Swift 5.7, iOS 16

Usage: textView.visibleLineCount (akin to textView.visibleSize)

extension UITextView {

    func heightThatFits(width: CGFloat) -> CGFloat {
        sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)).height
    }
    
    func sizeThatFits(width: CGFloat) -> CGSize {
        CGSize(width: width, height: heightThatFits(width: width))
    }
    
    var visibleLineCount: Int {
        Int(heightThatFits(width: bounds.width) / (font?.lineHeight ?? 1.0))
    }

}
kwiknik
  • 570
  • 3
  • 7
0

Since TextKit 2 has been introduced as the default layout system in iOS 16, accessing layoutManager instead of textLayoutManager will cause the UITextView to revert back to using TextKit 1.

Here's an extension for calculating the number of lines in a UITextView that's compatible with TextKit 2:

extension UITextView {
    func numberOfLines() -> Int {
        var numberOfLines = 0
        if let textLayoutManager = self.textLayoutManager {
            textLayoutManager.enumerateTextLayoutFragments(
                from: nil,
                options: [.ensuresLayout, .ensuresExtraLineFragment]) { fragment in
                numberOfLines += fragment.textLineFragments.count
                return true
            }
        }
        return numberOfLines
    }
}
adamup
  • 1,508
  • 19
  • 29