Reputation: 38142
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
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
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
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
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
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
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.
You can use storyboard with static cell height to design it as you want. Make UITextView scroll enable: false (disable scroll).
In viewDidLoad add your estimated row height and your textView delegate.
override func viewDidLoad() {
super.viewDidLoad()
quoteTextView.delegate = self
tableView.estimatedRowHeight = 142
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
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()
}
}
}
}
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
Upvotes: 8
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);
}
Upvotes: 0
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
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
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
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
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
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
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
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
Reputation: 16709
Use this (where _text_v is your text view):
-(NSInteger) linesCount {
return _text_v.contentSize.height/_text_v.font.lineHeight;
}
Upvotes: 5