Limit the number of line in UITextView - ios

Question
Is there any way to "accurately" limit the number of line in UITextView for target iOS 5.0?
What I had tried
As I had search in stack overflow. I had found these question been ask before in links below.
In UITextView, how to get the point that next input will begin another line
Limit the number of lines for UITextview
Limit number of lines in UITextView
But I still can't get the accurate number of line in UITextView when I tried to decide whether to return YES or NO in textView:shouldChangeTextInRange:replacementText:.
I had tried used the code which is the answer of Limiting text in a UITextView and the code after modified (remove -15 in the answer) is showing below.
- (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,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;
}
I also figure out a way to get the number of line in UITextView. The way is to calculate by contentSize property like textView.contenSize.height/font.lineHeight. This method can get the accurate number of lines in UITextView. But the problem is that contentSize get in textView:shouldChangeTextInRange:replacementText: and textViewDidChange: is the old contentSize. So I still can't limit the number of lines in UITextView.
Solution I used
This is kind of workaround but at least it work.
Step 1
At first you need to create a temporary new UITextView with all the same as the original UITextView but setting the temporary UITextView hidden in .xib file. In this sample code I name the temporary UITextView as tempTextInputView
Step 2
Add new referencing outlet to .h file like
#property (retain, nonatomic) IBOutlet UITextView *tempTextInputView;// Use to calculate the number of lines in UITextView with new text
Step 3
Add code below.
- (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;
}
}
Discussion
maxLinesInTextView is an int variable represent the maximum number of lines you want.
I use a temporary UITextView to setting new text is because when I setting the new text simply in the original UITextView, I got some problem when I typing in ChuYin(注音) keyboard which is a Traditional Chinese input method.
I still using textView:shouldChangeTextInRange:replacementText: but not textViewDidChange: is because I got some problem when cache the text before modify with a global NSString and replace the UITextView.text with that global NSString in textViewDidChange:.

Here's how you can use the UITextViewDelegate shouldChangeTextInRange: method to limit the text entry to the height of the text view:
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
// Combine the new text with the old
let combinedText = (textView.text as NSString).stringByReplacingCharactersInRange(range, withString: text)
// Create attributed version of the text
let attributedText = NSMutableAttributedString(string: combinedText)
attributedText.addAttribute(NSFontAttributeName, value: textView.font, range: NSMakeRange(0, attributedText.length))
// Get the padding of the text container
let padding = textView.textContainer.lineFragmentPadding
// Create a bounding rect size by subtracting the padding
// from both sides and allowing for unlimited length
let boundingSize = CGSizeMake(textView.frame.size.width - padding * 2, CGFloat.max)
// Get the bounding rect of the attributed text in the
// given frame
let boundingRect = attributedText.boundingRectWithSize(boundingSize, options: NSStringDrawingOptions.UsesLineFragmentOrigin, context: nil)
// Compare the boundingRect plus the top and bottom padding
// to the text view height; if the new bounding height would be
// less than or equal to the text view height, append the text
if (boundingRect.size.height + padding * 2 <= textView.frame.size.height){
return true
}
else {
return false
}
}

As I have mentioned in my answer here, I advise against using shouldChangeCharactersInRange: since it is invoked before the text is actually changed.
Using the textViewDidChangeMethod: makes more sense, since it is invoked after the text actually changes. From there you can easily decide what to do next.

One options is to modify the textView yourself in the shouldChangeTextInRange delegate method and always return NO (because you already did the work for it).
- (BOOL)textView:(UITextView *)aTextView shouldChangeTextInRange:(NSRange)aRange replacementText:(NSString*)aText
{
NSString* oldText = aTextView.text;
NSString* newText = [aTextView.text stringByReplacingCharactersInRange:aRange withString:aText];
aTextView.text = newText;
if(/*aTextView contentSize check herer for number of lines*/)
{
//If it's now too big
aTextView.text = oldText;
}
return NO
}

Related

hyperlink to section of text in uitextview

I am trying to set up a 'Frequently Asked Questions' section on a UITextView. How do I link a line of text on a UITextView so that when the user clicks on it, the UITextView scrolls to a section of text in the same view. I would also like to underline the text and change the text color to blue.
try TTTAttributedLabel
TTTAttributedLabel allows you to automatically detect links for dates, addresses, links, phone numbers, transit information, or allow you to embed your own.
label.enabledTextCheckingTypes = NSTextCheckingTypeLink; // Automatically detect links when the label text is subsequently changed
label.delegate = self; // Delegate methods are called when the user taps on a link (see `TTTAttributedLabelDelegate` protocol)
label.text = #"Fork me on GitHub! (http://github.com/mattt/TTTAttributedLabel/)"; // Repository URL will be automatically detected and linked
NSRange range = [label.text rangeOfString:#"me"];
[label addLinkToURL:[NSURL URLWithString:#"http://github.com/mattt/"] withRange:range]; // Embedding a custom link in a substring
You need to first fetch the click event of the text "Frequentry asked questions". on the click event you need to make code for scrolling.
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange
{
//Set your character range here
// if match return TRUE .
// else return FALSE.
}
On successful character range fetch scroll your textView to the questions by using this method.
CGPoint bottomOffset;
bottomOffset = CGPointMake(0,(Y value of the question));
[self.chatOutput setContentOffset:bottomOffset animated:YES];
This method will scroll the uitextview to the position of your question.
NSMutableAttributedString * str = [[NSMutableAttributedString alloc] initWithString:#"Google"];
[str addAttribute: NSLinkAttributeName value: #"http://www.google.com" range: NSMakeRange(0, str.length)];
yourTextField.attributedText = str;

How to shift subview of UITextView by size of one line when cursor goes to next line?

I have a textView and I added an image to this textView. When i write text and go to next line I want to shift this image down by size of one line.. Similar like in twitter app when you create a tweet with picture. In this code I can shift picture by line, but my problem is it shifts to late (only on second symbol of next line).. I tried to use different values in CGSizeMake but line was shifted too early or too late.. Any recommendations?
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range
replacementText:(NSString *)text
{
CGSize constrainedSize = CGSizeMake(self.textView.contentSize.width, self.textView.contentSize.height);
UIFont *fontText = [UIFont systemFontOfSize:17];
CGRect textRect = [textView.text boundingRectWithSize:constrainedSize
options:NSStringDrawingUsesLineFragmentOrigin
attributes:#{NSFontAttributeName:fontText}
context:nil];
size = textRect.size;
CGFloat height = ceilf(size.height);
CGFloat width = ceilf(size.width);
CGRect frame = CGRectMake(0.0f, height+50, 100.0f, 100);
_imageView.frame=frame;
return YES;
}
You are hardcoding your font size. Maybe the text field has a different font. Use textField.font instead.
Also, you should simplify your code and eliminate the unused variables, such as width.
Finally, you should set a breakpoint where you calculate the size and keep checking if it is correct. Based on that it should be easy to debug.
Also, please note that textField.text does not yet contain the text that is going to be added. Before checking the size, you would have to first construct the new resulting string yourself.
NSString *newText = [textField.text
stringByReplacingCharactersInRange:range withString:text];
I think you should add your Image as subview to superview of your textView.
In shouldChangeTextInRange: fit frame of textView to content of textView.
Then just use KVO for catch moments, when frame of textView was changed. So, now you can keep top border of image on bottom border of textview, even animated!
I wont write code instead you, but it sound very easily and interesting, right?

How to get n lines of text from UITextView in iOS

In my application i need to restrict to user enter 7 lines only in UITextView. When he tries enter text in 8 line we need to stop allowing editing and show an alert message. Its working fine by using CGSize of getting UITextView text.
But when user enters text in UITextView as Paste its not allowing if text is more than 7 lines. As per requirement i need to get the 7 lines of Text from entred (Copied & Pasted ) in to UITextView.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)string
{
NSString *temp = [textView.text stringByReplacingCharactersInRange:range withString:string]
CGSize size = [temp sizeWithFont:textView.font constrainedToSize:CGSizeMake(textView.frame.size.width,999) lineBreakMode:UILineBreakModeWordWrap];
int numLines = size.height / textView.font.lineHeight;
if (numLines <= 8)
{
return true;
}
//Alert
return false;
}
Please help me in this issue.
Thanks in Advance.
You could use the UITextViewTextDidChangeNotification notification to alert you on other changes like text being pasted in. (referenced at the bottom of this page)
Or, there is also this handy category on UITextView that allows it to work with the UITextField UIControl events that can be found here.

Getting the Y coordinate for every occurrence of a particular string in a UITextView for line numbering

I'm trying to get the y positions of every \n character in a UITextView so I can align line numberings alongside the UITextView for accurate line numbers. What I'd like to do is get an array of the Y coordinates of every occurrence of \n.
At the moment, I can get the CGSize for the current word, but I'm not quite sure how I can adjust this in order to get the Y coordinate of every occurence of a particular word.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
NSRange aRange = self.editorText.selectedRange;
if(range.location<self.editorText.text.length)
{
NSString * firstHalfString = [self.editorText.text substringToIndex:range.location];
NSString *temp = #"s";
CGSize s1 = [temp sizeWithFont:[UIFont systemFontOfSize:15]
constrainedToSize:CGSizeMake(self.view.bounds.size.width - 40, MAXFLOAT) // - 40 For cell padding
lineBreakMode:UILineBreakModeWordWrap]; // enter you textview font size
CGSize s = [firstHalfString sizeWithFont:[UIFont systemFontOfSize:15]
constrainedToSize:CGSizeMake(self.view.bounds.size.width - 40, MAXFLOAT) // - 40 For cell padding
lineBreakMode:UILineBreakModeWordWrap]; // enter you textview font size
//Here is the frame of your word in text view.
NSLog(#"xcoordinate=%f, ycoordinate =%f, width=%f,height=%f",s.width,s.height,s1.width,s1.height);
CGRect rectforword = CGRectMake(s.width, s.height, s1.width, s1.height);
// rectforword is rect of yourword
}
else
{
// Do what ever you want to do
}
return YES;
}
The main part I'm not sure about is getting each occurrence of the character in such a way that I can retrieve its coordinates.
Is there a way to do this? Or is there an easier way for me to implement line numbering?
Is custom parsing your NSString and appending #) to the start of each line an option?
It's a non-trivial bit of string parsing and manipulating to accomplish, but it has the added benefit of not having to care at all about yCoordinates and other messy layout based parameters.

UITextView get the current line

Is there a way (crazy hacks welcome) to get the current line as a string of a UITextView? This would include word wrapping, etc. For example, in this case:
The method would return "stack overflow. Isn't it great? I" because that is the current line based on the cursor.
It could also return "This is a test I made for" or "think so", based on the position of the cursor. I have tried working with both the UITextView methods and those of UITextInput protocol.
EDIT:
Here is the code I have attempted to use. The reason I need to find the string is to get it's length, so this is why you'll see UI based code.
NSRange location = self.textView.selectedRange;
NSString *searchString = [self.textView.text substringWithRange:NSMakeRange(0, location)];
CGSize currentStringDimensions = [searchString sizeWithFont:self.textView.font constrainedToSize:CGSizeMake(self.textView.frame.size.width, self.textView.frame.size.height) lineBreakMode:NSLineBreakByWordWrapping];
float numberOfRows = (currentStringDimensions.width/(self.textView.frame.size.width));
float left = (float)(numberOfRows - (int)numberOfRows) * (self.textView.frame.size.width);
This doesn't work, however. I think it might have something to with words being wrapped or the differently sized characters, but the left value is inconsistent or off after the first line.
The following code solution seem to be working. The "self" in this code refers to an instance of UITextView.
- (NSString *) getLineString:(NSRange)range
{
NSLayoutManager *manager = self.layoutManager;
// Convert it to a glyph range
NSRange matchingGlyphRange = [manager glyphRangeForCharacterRange:range actualCharacterRange:NULL];
// line fragment rect at location range
CGRect rect = [manager lineFragmentRectForGlyphAtIndex:matchingGlyphRange.location effectiveRange:nil];
// obtain the line range for the line fragment rect
NSRange lineRange = [manager glyphRangeForBoundingRect:rect inTextContainer:self.textContainer];
// extract the string out from lineRange
return [self.text substringWithRange:lineRange];
}
// ... later
NSLog(#"line %#", [self getLineString:self.selectedRange]);
This worked for me (self = the UITextView)
func getLineString() -> String {
return (self.text! as NSString).substringWithRange((self.text! as NSString).lineRangeForRange(self.selectedRange))
}
Swift 5 extension version of Gil's answer:
extension UITextView {
func getLineString() -> String {
guard let text = text else { return "" }
return (text as NSString).substring(with: (text as NSString).lineRange(for: self.selectedRange))
}
}
I ended up using the caretRect method of UITextInput to get the offset from the left. Worked flawlessly.

Resources