How to Read Number of lines in UITextView - objective-c

I am using UITextView In my View , I have requirement to count number of line contained by textview am using following function to read '\n' . However this works only when return key is pressed from keyboard , but in case line warapper (when i type continuous characters i wont get new line char ) . How do i read new char when line is changed without hitting return key ?? Anybody has nay idea how to .. please share it ..
I am follwing this link Link
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range
replacementText:(NSString *)text
{
// Any new character added is passed in as the "text" parameter
if ([text isEqualToString:#"\n"]) {
// Be sure to test for equality using the "isEqualToString" message
[textView resignFirstResponder];
// Return NO so that the final '\n' character doesn't get added
return NO;
}
// For any other character return YES so that the text gets added to the view
return YES;
}

In iOS 7, it should exactly be:
float rows = (textView.contentSize.height - textView.textContainerInset.top - textView.textContainerInset.bottom) / textView.font.lineHeight;

You can look at the contentSize property of your UITextView to get the height of the text in pixels, and divide by the line height spacing of the UITextView's font to get the number of text lines in the total UIScrollView (on and off screen), including both wrapped and line broken text.

extension NSLayoutManager {
var numberOfLines: Int {
guard let textStorage = textStorage else { return 0 }
var count = 0
enumerateLineFragments(forGlyphRange: NSMakeRange(0, numberOfGlyphs)) { _, _, _, _, _ in
count += 1
}
return count
}
}
Get number of lines in textView:
let numberOfLines = textView.layoutManager.numberOfLines

Just for reference...
Another way you can get the number of lines and also the content, is splitting the lines in an array using the same method mentioned on the answer edit by BoltClock.
NSArray *rows = [textView.text componentsSeparatedByString:#"\n"];
You can iterate through the array to get the content of each line and you can use the [rows count] to get the exact number of rows.
One thing to keep in mind is that empty lines will be counted as well.

Related

NSTextView select specific line

I am on Xcode 10, Objective-C, macOS NOT iOS.
Is it possible to programmatically select a line in NSTextView if the line number is given? Not by changing any of the attributes of the content, just select it as a user would do it by triple clicking.
I know how to get selected text by its range but this time i need to select text programmatically.
I've found selectLine:(id) but it seems to be for an insertion point.
A pointer to the right direction would be great and very much appreciated.
The Apple documentation here https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextLayout/Tasks/CountLines.html should be useful for what you're attempting to do.
In their example of counting lines of wrapped text they use the NSLayoutManager method lineFragmentRectForGlyphAtIndex:effectiveRange https://developer.apple.com/documentation/appkit/nslayoutmanager/1403140-linefragmentrectforglyphatindex to find the effective range of the line, then increase the counting index to the end of that range (i.e. starting at the next line). With some minor modification, you can use it to find the range of the line you'd like to select, then use NSTextView's setSelectedRange: to select it.
Here's it modified to where I believe it would probably work for what you're attempting to accomplish:
- (void)selectLineNumber:(NSUInteger)lineNumberToSelect {
NSLayoutManager *layoutManager = [self.testTextView layoutManager];
NSUInteger numberOfLines = 0;
NSUInteger numberOfGlyphs = [layoutManager numberOfGlyphs];
NSRange lineRange;
for (NSUInteger indexOfGlyph = 0; indexOfGlyph < numberOfGlyphs; numberOfLines++) {
[layoutManager lineFragmentRectForGlyphAtIndex:indexOfGlyph effectiveRange:&lineRange];
// check if we've found our line number
if (numberOfLines == lineNumberToSelect) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self.testTextView setSelectedRange:lineRange];
}];
break;
}
indexOfGlyph = NSMaxRange(lineRange);
}
}
Then you could call it with something like:
[self selectLineNumber:3];
Keep in mind that we're starting at index 0. If you pass in a lineNumberToSelect that is greater than the numberOfLines, it should just be a no-op and the selection should remain where it is.
Thanks #R4N! Your answer helped me figure out how jump to specific line with NSTextView. I converted your Obj-C to Swift 5:
func selectLineNumber(lineNumberToJumpTo: Int) {
let layoutManager = textView.layoutManager!
var numberOfLines = 1
let numberOfGlyphs = layoutManager.numberOfGlyphs
var lineRange: NSRange = NSRange()
var indexOfGlyph = 0
while indexOfGlyph < numberOfGlyphs {
layoutManager.lineFragmentRect(forGlyphAt: indexOfGlyph, effectiveRange: &lineRange, withoutAdditionalLayout: false)
// check if we've found our line number
if numberOfLines == lineNumberToJumpTo {
textView.selectedRange = lineRange
textView.scrollRangeToVisible(lineRange)
break
}
indexOfGlyph = NSMaxRange(lineRange)
numberOfLines += 1
}
}

Check if text in UITextView went to new line due to word wrap

How can I check if the text in a UITextView went to the next line due to word wrap?
I currently have code to check if the user enters a new line (from the keyboard).
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text;
{
if ( [text isEqualToString:#"\n"] ) {
...
}
return YES;
}
You can use the contentSize.height property of UITextView to determine how 'tall' your textview is. You can then divide by the lineHeight to figure out the number of lines. #nacho4d provided this nifty line of code that does just that:
int numLines = textView.contentSize.height/textView.font.lineHeight;
Refer to this question for more info:
How to read number of lines in uitextview
EDIT:
So you could do something like this inside -textViewDidChange: in order to check for a line change.
int numLines = textView.contentSize.height / textView.font.lineHeight;
if (numLines != numLinesInTextView){
numLinesInTextView = numLines;//numLinesInTextView is an instance variable
//then do whatever you need to do on line change
}

Is there a way to limit the width of text in UITextView

I have UITextView where I need to control where text start and end.
This because I use bubble as background image for UITextView and I need to display the text inside the bubble image.
So far,I have been able to control where text start using:
self.contentOffset = (CGPoint){.x = -10, .y = -10};
but I have no clue how to control where text end. Currently, by default it uses all UITextView width.
Will the bubble grow in height as the text stretches? Or will the bubble be limited to 1 line? Either way, I think you should use a different approach to deal with this problem.
Use -sizeWithFont:constainedToSize:lineBreakMode: on an NSString instance to figure out what the size is of the text. If the size is greater then the value allowed by you, stop allowing input (if that's your goal) or increase hight of the bubble and allow text to continue on next line.
You would typically perform these recalculations upon text input, which means you'd implement the UITextViewDelegate method -textView:shouldChangeTextInRange:replacementText: according to the above suggestions.
-- or --
Just limiting the size of the textView and setting the lineBreakMode & numberOfLines might also be sufficient for your purposes.
define #define TEXT_LENGTH 25 // Whatever your limit is
The UITextView calls this method whenever the user types a new character or deletes an existing character.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
NSUInteger newLength = (textView.text.length - range.length) + text.length;
if(newLength <= TEXT_LENGTH)
{
return YES;
} else {
NSUInteger mySpace = TEXT_LENGTH - (textView.text.length - range.length);
textView.text = [[[textView.text substringToIndex:range.location]
stringByAppendingString:[text substringToIndex: mySpace]]
stringByAppendingString:[textView.text substringFromIndex:(range.location + range.length)]];
return NO;
}
}
Use replacementText not replacementString in method, so careful with the different between replacementString and replacementText.

UITEXTVIEW: Get the recent word typed in uitextview

I want to get the most recent word entered by the user from the UITextView.
The user can enter a word anywhere in the UITextView, in the middle or in the end or in the beginning. I would consider it a word when the user finishes typing it and presses a space and does any corrections using the "Suggestions from the UIMenuController".
Example: User types in "kimd" in the UITextView somewhere in the middle of text, he gets a popup for autocorrection "kind" which he does. After he does that, I want to capture "kind" and use it in my application.
I searched a lot on the internet but found solutions that talk about when the user enters text in the end. I also tried detecting a space and then doing a backward search until another space after some text is found, so that i can qualify it as a word. But I think there may be better ways to do this.
I have read somewhere that iOS caches the recent text that we enter in a text field or text view. If I can pop off the top one , that's all I want. I just need handle to that object.
I would really appreciate the help.
Note: The user can enter text anywhere in UItextview. I need the most recent entered word
Thanks.
//This method looks for the recent string entered by user and then takes appropriate action.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
//Look for Space or any specific string such as a space
if ([text isEqualToString:#" "]) {
NSMutableCharacterSet *workingSet = [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
NSRange newRange = [self.myTextView.text rangeOfCharacterFromSet:workingSet
options:NSBackwardsSearch
range:NSMakeRange(0, (currentLocation - 1))];
//The below code could be done in a better way...
UITextPosition *beginning = myTextView.beginningOfDocument;
UITextPosition *start = [myTextView positionFromPosition:beginning offset:currentLocation];
UITextPosition *end = [myTextView positionFromPosition:beginning offset:newRangeLocation+1];
UITextRange *textRange = [myTextView textRangeFromPosition:end toPosition:start];
NSString* str = [self.myTextView textInRange:textRange];
}
}
Here is what I would suggest doing, might seem a little hacky but it would work just fine:
First in .h conform to the UITextViewDelegate and set your text view's delegate to self like this:
myTextView.delegate = self;
and use this code:
- (void)textViewDidChange:(UITextView *)textView { // Delegate method called when any text is modified
if ([textView.text substringFromIndex: [textView.text length] - 1]) { // Gets last character of the text view's text
NSArray *allWords = [[textView text] componentsSeparatedByString: #" "]; // Gets the text view's text and fills an array with all strings seperated by a space in text view's text, basically all the words
NSString *mostRecentWord = [allWords lastObject]; // The most recent word!
}
}
I use this code to get the word behind the #-sign:
- (void)textViewDidChange:(UITextView *)textView {
NSRange rangeOfLastInsertedCharacter = textView.selectedRange;
rangeOfLastInsertedCharacter.location = MAX(rangeOfLastInsertedCharacter.location - 1,0);
rangeOfLastInsertedCharacter.length = 1;
NSString *lastInsertedSubstring;
NSString *mentionSubString;
if (![textView.text isEqualToString:#""]) {
lastInsertedSubstring = [textView.text substringWithRange:rangeOfLastInsertedCharacter];
if (self.startOfMention > 0 || self.startOfHashtag > 0) {
if ([lastInsertedSubstring isEqualToString:#" "] || (self.startOfMention > textView.selectedRange.location || self.startOfHashtag > textView.selectedRange.location)) {
self.startOfMention = 0;
self.lenthOfMentionSubstring = 0;
}
}
if (self.startOfMention > 0) {
self.lenthOfMentionSubstring = textView.selectedRange.location - self.startOfMention;
NSRange rangeOfMentionSubstring = {self.startOfMention, textView.selectedRange.location - self.startOfMention};
mentionSubString = [textView.text substringWithRange:rangeOfMentionSubstring];
dhDebug(#"mentionSubString: %#", mentionSubString);
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
}
}
}
Simple extension for UITextView:
extension UITextView {
func editedWord() -> String {
let cursorPosition = selectedRange.location
let separationCharacters = NSCharacterSet(charactersInString: " ")
let beginRange = Range(start: text.startIndex.advancedBy(0), end: text.startIndex.advancedBy(cursorPosition))
let endRange = Range(start: text.startIndex.advancedBy(cursorPosition), end: text.startIndex.advancedBy(text.characters.count))
let beginPhrase = text.substringWithRange(beginRange)
let endPhrase = text.substringWithRange(endRange)
let beginWords = beginPhrase.componentsSeparatedByCharactersInSet(separationCharacters)
let endWords = endPhrase.componentsSeparatedByCharactersInSet(separationCharacters)
return beginWords.last! + endWords.first!
}
}

Limiting text in a UITextView

I am trying to limit the text input into a UITextView in cocoa-touch. I really want to limit the amount of rows rather than the number of characters. So far I have this to count the amount of rows:
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
if([text isEqualToString:#"\n"]) {
rows++;
}
NSLog(#"Rows: %i", rows);
return YES;
}
However this doesn't work if the line is automatically wrapped rather than the user pressing the return key. Is there a way to check if the text was wrapped similar to checking for "\n"?
Thanks.
Unfortunately, using NSString -stringWithFont:forWidth:lineBreakMode: doesn't work - which ever wrap mode you choose, the text wraps with a width that is less than the current width, and the height becomes 0 on any overflow lines. To get a real figure, fit the string into a frame that is taller than the one you need - then you'll get a height that is greater than your actual height.
Note my fudge in this (subtracting 15 from the width). This might be something to do with my views (I have one within another), so you might not need it.
- (BOOL)textView:(UITextView *)aTextView shouldChangeTextInRange:(NSRange)aRange replacementText:(NSString*)aText
{
NSString* newText = [aTextView.text stringByReplacingCharactersInRange:aRange withString:aText];
// TODO - find out why the size of the string is smaller than the actual width, so that you get extra, wrapped characters unless you take something off
CGSize tallerSize = CGSizeMake(aTextView.frame.size.width-15,aTextView.frame.size.height*2); // pretend there's more vertical space to get that extra line to check on
CGSize newSize = [newText sizeWithFont:aTextView.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
if (newSize.height > aTextView.frame.size.height)
{
[myAppDelegate beep];
return NO;
}
else
return YES;
}