Abhinav
Abhinav

Reputation: 38142

Counting the number of lines in a UITextView, lines wrapped by frame size

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]);

Upvotes: 39

Views: 51065

Answers (16)

adamup
adamup

Reputation: 1538

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
    }
}

Upvotes: 1

kwiknik
kwiknik

Reputation: 885

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))
    }

}

Upvotes: 0

Softlabsindia
Softlabsindia

Reputation: 921

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

Upvotes: 3

Himanshu padia
Himanshu padia

Reputation: 7712

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.

Upvotes: 39

revilo
revilo

Reputation: 179

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
    }
}

Upvotes: 5

Egzon P.
Egzon P.

Reputation: 4768

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

Upvotes: 8

MichaelMao
MichaelMao

Reputation: 571

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

Upvotes: 0

T&#224; Truhoada
T&#224; Truhoada

Reputation: 594

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
    }
}

Upvotes: 10

Franc
Franc

Reputation: 371

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!).

Upvotes: 4

Yeesha
Yeesha

Reputation: 141

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
}

Upvotes: 13

castillejoale
castillejoale

Reputation: 519

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)

Upvotes: 15

Juan Boero
Juan Boero

Reputation: 6417

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.

Upvotes: 17

Luke Chase
Luke Chase

Reputation: 391

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

Upvotes: 12

LiangWang
LiangWang

Reputation: 8836

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)

Upvotes: 7

Jano
Jano

Reputation: 63667

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;

Upvotes: 32

Max
Max

Reputation: 16709

Use this (where _text_v is your text view):

-(NSInteger) linesCount {
    return _text_v.contentSize.height/_text_v.font.lineHeight;
}

Upvotes: 5

Related Questions