UITextView word wrapping with maximum line-length and lines - ios

For an iOS application I am writing I am using an UITextView, where the user can insert a limited text.
To the textview there are 2 restrictions:
Lines can be no longer than 30 characters
There can be only 20 lines of text in the UITextView.
So in short, the maximum is 20 lines of 30 characters.
When a user is typing some text in the UITextView and the current sentence is 30 characters, I want it to automatically insert a new line \n (before the last word on that line) and force the last word and cursor to the line below.
When a user has 20 lines with 30 characters (or even simpler said: 20 lines, with 30 characters on the last line) I want the input to be blocked.
Now, most of this is fairly 'simple' but the code I have does not account for border cases, like inserting text in earlier lines.
I looked around the documentation from Apple, but I can not find a way to actually force this kind of Word-wrapping on a UITextView.
My try is to handle all this in the shouldChangeTextInRange delegate method (made the code a little more verbose, so it's a bit easier to read).
#define MAX_LENGTH_LINE 30
#define MAX_LENGTH_ROWS 20
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
// Check for backspaces, they should always be allowed?
if ([text length] == 0 && ![text isEqualToString:#"\n"])
return YES;
NSArray* lines = [textView.text componentsSeparatedByString:#"\n"];
// Check if there are a maximum of lines and the last line is already maxed out
NSString* lastLine = [lines objectAtIndex:[lines count] - 1];
if (([lines count] == MAX_LENGTH_ROWS) &&
(lastLine != nil) &&
([lastLine length] > MAX_LENGTH_LINE) &&
([text length] > 0))
return NO;
if ((lastLine != nil) &&
([lastLine length] > MAX_LENGTH_LINE))
{
NSRange range = [textView.text rangeOfString:#" " options:NSBackwardsSearch];
NSRange breakRange = [textView.text rangeOfString:#"\n" options:NSBackwardsSearch];
if (breakRange.location == NSNotFound)
breakRange = NSMakeRange(0, 1);
if (range.location == NSNotFound) {
range = NSMakeRange(0, 1);
}
if (range.location > breakRange.location)
{
textView.text = [textView.text stringByReplacingCharactersInRange:NSMakeRange(range.location, 1) withString:#"\n"];
}
else
{
textView.text = [textView.text stringByAppendingString:#"\n"];
}
}
if ([text isEqualToString:#"\n"])
{
if ([lines count] == MAX_LENGTH_ROWS)
return NO;
else {
return YES;
}
NSRange range = NSMakeRange(textView.text.length - 1, 1);
[textView scrollRangeToVisible:range];
}
return YES;
}
In the meanwhile I have been at this for a while and I lost it at the moment. Anyone who can give some pointers to just limit the UITextView to the 20 lines / 30 characters limitation I want?

This may be counter to your overall goals, but the simplest answer in my head is to reparse the string every time the user adds a character. Then it wouldn't matter where the character was added. Instead of doing all this in shouldChangeTextInRange: do it in textViewDidChange:. You will need to be a UITextViewDelegate and you will need a single class NSString to hold the last successful user text update in case your user is trying to add a character beyond the allowed limit.
Something like this:
-(void)textViewDidChange:(UITextView *)textView
{
//Get the textview without any new line characters
NSMutableString *temporaryString = [NSMutableString stringWithString:[textView.text stringByReplacingOccurrencesOfString:#"\n" withString:#" "]];
bool updatePossible = true;
int i = 0, numberOfLinesSoFar = 0;
while(i + 30 < [temporaryString length])
{
//Go 30 characters in and start the reverse search for a word separation
i += 30;
int j = i;
//Get the location of the final word separation in the current line
while(j >= i && [temporaryString characterAtIndex:j] != ' ')
{
j--;
}
//This means we found a word separation
if(j > i)
{
i = j;
[temporaryString replaceCharactersInRange:NSMakeRange(i,1) withString:#"\n"];
}
//We didn't find a word separation
else
{
//Here we will just have to break the line at 30.
[temporaryString insertString:#"\n" atIndex:i];
}
numberOfLinesSoFar++;
//Check if we just wrote line 20 and still have characters to go.
if(numberOfLinesSoFar > 19 && i < [temporaryString length])
{
//Revert user change to the last successful character addition
textView.text = lastSuccessfulViewString;
updatePossible = false;
break;
}
}
if(updatePossible)
{
//If we are within the limits then update the global string (for undoing character additions) and the textview
textView.text = temporaryString;
lastSuccessfulViewString = temporaryString;
}
}
Now this won't allow the user to put in their own new line characters, but that could be handled with a couple if then statements.

After a bit of fiddling around, created a control which contains a UITextView as a subview.
I let this control handle the text wrapping and forward the delegate methods to the view registered as a delegate.
It might help others, so I am posting a link to BitBucket here.
PS. It's still very verbose, but that is to show how I solved this for my case.
https://bitbucket.org/depl0y/sbframework/src/master/SBFramework/Views/SBTextView?at=master

Related

iOS: How to limit number of lines of UITextView using Swift?

I am making an application in which there is an UITextView that only allows users to type in 2 lines of text. So the text will not reach the third line as the UITextView would ask it to stop. Does anyone know how to approach this in Swift programming? I have found there is a way to achieve this but written in Objective-C code
- (BOOL) textView:(UITextView *)textView
shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
NSString *newText = [textView.text stringByReplacingCharactersInRange:range withString:text];
_tempTextInputView.text = newText;
// Calcualte the number of lines with new text in temporary UITextView
CGRect endRectWithNewText = [_tempTextInputView caretRectForPosition:_tempTextInputView.endOfDocument];
CGRect beginRectWithNewText = [_tempTextInputView caretRectForPosition:_tempTextInputView.beginningOfDocument];
float beginOriginY = beginRectWithNewText.origin.y;
float endOriginY = endRectWithNewText.origin.y;
int numberOfLines = (endOriginY - beginOriginY)/textView.font.lineHeight + 1;
if (numberOfLines > maxLinesInTextView) {// Too many lines
return NO;
}else{// Number of lines will not over the limit
return YES;
}
}
I have been struggling with the equivalent code in Swift especially this line of code:
NSString *newText = [textView.text stringByReplacingCharactersInRange:range withString:text];
I just couldn't figure out how to deal with NSRange in Swift. Could someone please help me ?
Can you just set the frame size for two lines and limit the total number of characters that can be entered?

enumerateLineFragmentsForGlyphRange:withBlock: returns word fragments

I'm using a UITextView to display some text. In laying out the text, I enumerate the lines of text using the enumerateLineFragmentsForGlyphRange:withBlock: method.
NSInteger shrunkNumberOfLines = 3;
__block NSMutableString *shortenedText = [NSMutableString new];
__block NSInteger currentLine = 0;
__block BOOL needsTruncation = NO;
[detailsTableViewCell.descriptionTextView.layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(0, text.length) usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {
if (currentLine < shrunkNumberOfLines) {
NSRange stringRange = ((glyphRange.length + glyphRange.location) <= text.length) ? glyphRange : NSMakeRange(glyphRange.location, (text.length - glyphRange.location));
NSString *appendString = [text substringWithRange:stringRange];
NSLog(#"%#", appendString);
[shortenedText appendString:appendString];
currentLine += 1;
} else {
needsTruncation = YES;
*stop = YES;
}
}];
However, I'm running into a weird bug: oftentimes, the text that gets displayed in the textview doesn't line up with the text that I see in that appendString.
For example, the text in the textfield might say something like:
President Obama offered a
blueprint for deeper American
engagement in the Middle East.
...but looking at my NSLog statements, those appendStrings are something like:
President Obama offered a blu
eprint for deeper American en
gagement in the Middle East.
I've tried a bunch of things - playing with hyphenationFactor, making sure that the textContainerInsets are correct, etc - but I can't figure this out. What's causing invalid line breaks in the enumerateLineFragmentsForGlyphRange:withBlock: method?
While I'm still not sure what caused the underlying issue above, I've at least found something that solves the symptom: https://stackoverflow.com/a/19603172/686902

textView:shouldChangeTextInRange:replacementText: returning NO, but autocorrect ignores?

I'm having the following problem, and am not sure if this is an iOS bug, or I'm misunderstanding UITextViewDelegate callbacks. I'm able to reproduce this behavior on my device (iPad retina) and on the simulator only if I use the software keyboard (i.e. use my mouse to click the on-screen keyboard).
Consider the following contrived example project (Updated 1/9/14):
https://www.dropbox.com/s/1q3vqfnsmmbhnuj/AutocorrectBug.zip
It's a simple UITextView, with a view controller set as its delegate, with the following delegate method:
- (BOOL) textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
if ([text isEqualToString:#" "]
&& range.length == 0
&& (range.location == 0
|| [[textView.text substringWithRange:NSMakeRange(range.location-1, 1)] isEqualToString:#"\n"])) {
textView.backgroundColor = [UIColor yellowColor];
return NO;
}
return YES;
}
My intention with this code is that when the user types a space (edit: and that space is the first character on the line), the space is ignored and instead some other functionality happens, in this case the UITextView turns yellow.
Now consider these steps:
Tap in the text view to make it first responder
Type "Apple", hit return and note the positioning
Now, type a space (turning the text view yellow), type "Apple" again and hit return.
Expected: A yellow background and text reading:
Apple
Apple
Observed: A yellow background and text reading (due to an autocorrection):
Apple
Apple
It appears the autocorrection logic is ignoring the result of textView:shouldChangeTextInRange:replacementText:.
Is this expected behavior?
If so can it be worked around?
Edit 1/9/14: only the first space on a line should be ignored, further spaces should be processed as normal (i.e. inserted into the text), ruling out a brute force strip of spaces. Also I'm dealing with a large string (potentially hundreds of thousands of characters) in a text editor (meaning constant user typing), so analyzing the entire string with every keystroke isn't going to be performant.
Yes, I see it too on my iPad 7.0.3 simulator.
One way to solve it is to add this UITextViewDelegate method:
- (void)textViewDidChange:(UITextView *)textView
{
// eliminates spaces, including those introduced by autocorrect
if ([textView.text rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]].location != NSNotFound) {
textView.text = [textView.text stringByReplacingOccurrencesOfString:#" " withString:#""];
}
}
Update
It seems that this delegate method is called after autocorrection, just that the in the case you want to prevent (where "Apple" becomes " Apple") the replacement text is " Apple" and not " ". So to modify the implementation to prevent "\n " but allow other " " characters in your text, you could try comparing the first character of text to " " instead of comparing text to " ".
- (BOOL) textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
// compare first character to 'space' character by decimal value
if (text.length && [text characterAtIndex:0] == 32) {
// check if previous character is a newline
NSInteger locationOfPreviousCharacter = range.location - 1;
if (locationOfPreviousCharacter < 0 || [textView.text characterAtIndex:locationOfPreviousCharacter] == 10) {
textView.backgroundColor = [UIColor yellowColor];
return NO;
}
}
return YES;
}

Restrict the user to enter max 50 words in UITextView iOS

I am trying to restrict the user to enter max 50 words in UITextView. I tried solution by PengOne from this question1 . This works for me except user can enter unlimited chars in the last word.
So I thought of using regular expression. I am using the regular expression given by
VonC in this question2. But this does not allow me enter special symbols like , " # in the text view.
In my app , user can enter anything in the UITextView or just copy-paste from web page , notes , email etc.
Can anybody know any alternate solution for this ?
Thanks in advance.
This code should work for you.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
return textView.text.length + (text.length - range.length) <= 50;
}
Do as suggested in "question2", but add # within the brackets so that you can enter that as well. May need to escape it if anything treats it as a special character.
You use [NSCharacterSet whitespaceCharacterSet] to calculate word.
- (BOOL) textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
static const NSUInteger MAX_NUMBER_OF_LINES_ALLOWED = 3;
NSMutableString *t = [NSMutableString stringWithString: self.textView.text];
[t replaceCharactersInRange: range withString: text];
NSUInteger numberOfLines = 0;
for (NSUInteger i = 0; i < t.length; i++) {
if ([[NSCharacterSet whitespaceCharacterSet] characterIsMember: [t characterAtIndex: i]]) {
numberOfWord++;
}
}
return (numberOfWord < 50);
}
The method textViewDidChangeSelection: is called when a section of text is selected or the selection is changed, such as when copying or pasting a section of text.

how to perform backspace and delete?

I'm developing a code in where I need to perform a delete and backspace from UITextView.
I tried:
-(IBAction)delete:(id)sender{
uitextview.text =[uitextview.text substringToIndex:[uitextview.text length]-1];
}
this perform back space and remove last character but how to delete a character from specific location in uitextview on a particular cursor location.
Please help me out.
Use the selectedRange property of the UITextView. This indicates the cursor position and/or selected text (the length will be zero if no text is selected).
So, if the range.length is > 0, you can use stringByReplacingCharactersInRange:withString: to delete the selected text.
Otherwise, create a new range with location one less than your selected range and length of 1, and then remove this character.
You may need to re-set the selectedRange property to make sure the cursor ends up in the appropriate position. I imagine all this is done automatically when using the standard keyboard.
Thanks for ur suggest.I want to suggest one way to perform this work.
NSRange range = uitextview.selectedRange;
NSRange selectedRange = uitextview.selectedRange;
NSLog(#"%d",selectedRange.length);
NSLog(#"%d",selectedRange.location);
int temploc=selectedRange.location;
NSMutableString *text = [uitextview.text mutableCopy];
// NSLog(#"Length: %d Location: %d", range.length, range.location);
if (range.length > 0) {
[text deleteCharactersInRange:range];
}
if (range.length == 0 && range.location != 0) {
NSRange backward = NSMakeRange(range.location - 1, 1);
// NSLog(#"Length: %d Location: %d", backward.length, backward.location);
[text deleteCharactersInRange:backward];
}
uitextview.text = text;

Resources