Limiting text in a UITextView - cocoa-touch

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

Related

Equal sized cells in NSSegmentedControl

For my NSSegmentedControl, I use it do display a bar to control a NSTableView. I have code set up to control the size programmatically:
for (NSInteger i = 1; i <= numberOfSegments; i++) {
CGSize textSize = [[NSString stringWithFormat:#"Workspace %ld", (long)i] sizeWithAttributes:#{NSFontAttributeName: [NSFont systemFontOfSize:13.0f]}];
NSInteger segmentWidth = self.workspaceControl.frame.size.width / numberOfSegments;
if (textSize.width > segmentWidth) {
[self.workspaceControl setLabel:[NSString stringWithFormat:#"%ld", (long)i] forSegment:i - 1];
} else {
[self.workspaceControl setLabel:[NSString stringWithFormat:#"Workspace %ld", (long)i] forSegment:i - 1];
}
[self.workspaceControl setWidth:segmentWidth forSegment:i - 1];
}
This works, by a small problem occurs.
At the beginning (with one segment) it looks like this:
As I change the value, the right side gets clipped slightly.
And then back to one segment:
The constraints are as follows:
Im very puzzled by the clipping (probably because a couple pixels t0o large), but I have no idea how to fix it, or is there a better way to get evenly spaced cells?
In my testing the generated width constraints of the NSSegmentedControl segments appear to round to whole numbers despite setWidth(_ width: CGFloat, forSegment segment: Int) taking a CGFloat. Thus the total width of the segments can add up to more than the width of the control itself.
To get even segments pass segmentWidth through the floor. Also, set a manual width for the control and constrain it horizontally by the centerXAnchor instead of by the leading and trailing anchors to preserve its center-alignment.
Drawbacks: the width must be hard-coded and the left and right margins may not match your other views exactly.

Efficiently determine how much text can fit in a UILabel in IOS

I have an NSString and I want to know how much of that string will fit into a UILabel.
My code builds a test string by adding one character at a time from my original string. Each time I add a character I test the new string to see if it will fit into my label:
CGRect cutTextRect = [cutText boundingRectWithSize:maximumLabelSize options:NSStringDrawingUsesLineFragmentOrigin attributes:stringAttributes context:nil];
Then I compare the height of that rect to the height of my label to see if the string overflowed.
This works, but instruments shows me that the loop is taking up all my cpu time.
Can anyone think of or know of a faster way to do this?
Thanks!
While not the prettiest:
- (NSString *)stringForWidth:(CGFloat)width fullString:(NSString *)fullString
{
NSDictionary *attributes = #{NSFontAttributeName: label.font};
if ([fullString sizeWithAttributes:attributes].width <= width)
{
return fullString;
}
// Might be worth researching more regarding 'average' char size
CGFloat approxCharWidth = [#"N" sizeWithAttributes:attributes].width;
NSInteger approxNumOfChars = (NSInteger)(width / approxCharWidth);
NSMutableString *resultingString = [NSMutableString stringWithString:[fullString substringToIndex:approxNumOfChars]];
CGFloat currentWidth = [resultingString sizeWithAttributes:attributes].width;
if (currentWidth < width)
{
// Try to 'sqeeze' another char.
while (currentWidth < width && approxNumOfChars < fullString.length)
{
approxNumOfChars++;
[resultingString appendString:[fullString substringWithRange:NSMakeRange(approxNumOfChars - 1, 1)]];
currentWidth = [resultingString sizeWithAttributes:attributes].width;
}
}
// String might be oversized
if (currentWidth > width)
{
while (currentWidth > width)
{
[resultingString deleteCharactersInRange:NSMakeRange(resultingString.length - 1, 1)];
currentWidth = [resultingString sizeWithAttributes:attributes].width;
}
}
// If dealing with UILabel, it's safer to have a smaller string than 'equal',
// 'clipping wise'. Otherwise, just use '<=' or '>=' instead of '<' or '>'
return [NSString stringWithString:resultingString];
}
There are a couple of loops, but each one is a 'fine tuning' and should only run a small number of times.
One way to improve efficiency is to get a better starting point than calculating be how many N's can fit in a given width.
I'm open to suggestions about that.
-Edit:
Regarding multiline label, once I know a given text for width, I can expect the following text (if any) will go to the next line.
In other words, getting 'text for width' is the tricky part, 'width for text' we get for free.

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.

How to Read Number of lines in UITextView

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.