I need to move text that the user has entered into a large multi-line UITextView into a smaller (but still multi-line) UITextView*. If the user has entered more text than will display in the smaller view, I want to truncate the text so that it fits with all the (truncated) text visible. (Neither the large UITextView nor the smaller one should scroll.)
What's the best way to do this?
I can use a loop, shortening the string by a character each time, and then use NSString's sizeWithFont: constrainedToSize: lineBreakMode: to find out the height this shorter string would need, and then compare that against the height I have available in my smaller UITextView, ending the loop when the string will fit - but that seems slow and awkward. There must be a better way.
I'd like to just tell the destination UITextView to truncate its displayText member as it displays it on screen, but I've not been able to find a way to do that.
*More context on this, from a comment I made below:
I've got a landscape app. I change the layout of the view depending on the photo the user chooses. If it's a landscape photo, the caption is smaller - just a line at the bottom of the photo. If she chooses a portrait photo, then there's plenty of space I can use for the caption at the side of the photo, so the caption is bigger.
If the user changes her photo orientation from portrait to landscape, then I want to truncate the text and then allow her to edit it so that it makes sense. I could just zap it, but I'd prefer to preserve it to minimize her typing.
I wrote the following recursive method and public API to do this properly. The ugly fudge factor is the subject of this question.
#define kFudgeFactor 15.0
#define kMaxFieldHeight 9999.0
// recursive method called by the main API
-(NSString*) sizeStringToFit:(NSString*)aString min:(int)aMin max:(int)aMax
{
if ((aMax-aMin) <= 1)
{
NSString* subString = [aString substringToIndex:aMin];
return subString;
}
int mean = (aMin + aMax)/2;
NSString* subString = [aString substringToIndex:mean];
CGSize tallerSize = CGSizeMake(self.frame.size.width-kFudgeFactor,kMaxFieldHeight);
CGSize stringSize = [subString sizeWithFont:self.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
if (stringSize.height <= self.frame.size.height)
return [self sizeStringToFit:aString min:mean max:aMax]; // too small
else
return [self sizeStringToFit:aString min:aMin max:mean];// too big
}
-(NSString*)sizeStringToFit:(NSString*)aString
{
CGSize tallerSize = CGSizeMake(self.frame.size.width-kFudgeFactor,kMaxFieldHeight);
CGSize stringSize = [aString sizeWithFont:self.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
// if it fits, just return
if (stringSize.height < self.frame.size.height)
return aString;
// too big - call the recursive method to size it
NSString* smallerString = [self sizeStringToFit:aString min:0 max:[aString length]];
return smallerString;
}
This isn't actually a fix but it does provide a good starting poing for the calculation.
If you use NSString's sizeWithFont: constrainedToSize: lineBreakMode: you get a vertical height for your text. If you divide that by your font's leading height, you get the number of lines in the whole string. Dividing [NSString count] by that number gives you an approximation to number of characters per line. This assumes the string is homogeneuous and will be inaccurate if someone types (e.g.) 'iiiiiiiiiii..." as oposed to "MMMMMMMMM...".
You can also divide you bounding box by the relevent font's leading height to get the number of lines that fit within your bounding box.
Multiplying characters per line by number of lines gives you a starting point for finding text that fits.
You could calculate the margin for error in this figure by doing the same calculation for those 'iiiiii...' and "MMMMMM...'" strings.
I would suggest taking a slightly different approach and seeing if you can use a UILabel instead of the smaller UITextView.
UILabels can be setup to be multi-line like a UITextView through their numberOfLines property.
UILabels also have a lineBreakMode property and I believe that the default value of that property will do the exact truncation effect that you are looking for.
I think Jonathan was on to something about the UILabel...
So, the user finishes editing the UITextView, you get the string of text and pass it to the UILabel. You change the alpha of the UITextView to 0 and/or remove it from superview. Possibly store the untruncated full text in an ivar.
UILabels are not "editable", however you can detect a touch with a UILabel (or it's superview).
When you detect the touch on the UILabel, you simply restore the hidden UITextView and restore the string you saved.
Sometimes the SDK is a pain, but it almost always wins the fight. Many times, it is better to adjust your design to UIKit conventions
Related
I have some text coming from server. It may be single line or multiline text. I have to display the text on UILabel, which is no problem for me. The problem is, I have to display UIButton on finding a particular substring of the same text. For example the text is Nitish\n435-234-6543\nIndia which is being displayed as follows :
Nitish
435-234-6543
India
So, when I find 435-234-6543 I have to display UIButton on 435-234-6543.
Notes:
The text is dynamic - coming from server. Above is only an example.
UIButton will be a subview of UILabel.
I tried different ways like OHAttributedLabel, rectForLetterAtIndex and this too. But not getting success. What my idea is, to create the button when substring is found and to set the frame of button based on NSRange of substring. Is this a possibility? How can it be done? Or is there some other way to do this?
I guess it is the approach I am worried about.
-->I have tried to calcluate position but didnt get success. Seems lots of work to calcualate position. One immediate solution come to my mind is why are you not taking one or two labels and one button.
Suppose. You got dynamic string from webservice is: "Nitesh-56789-Test".
Find range of string suppose i.e. "56789".
String before starting location of that range assign to one label i.e. assign "Nitesh" to lable one.
Now add one custom button with our searched string as a text(56789).
Now make sure in main string there something after our substring or not. Here I mean after our search string "56789" still "Test" remain so assign it to third lable.
Here you have to figue out frame of all labels and button using dynamic height width calculation by using sizeWithFont method.
1) Easy solution:
Make your UILabel a UITextView and use the property dataDetectorTypes to have phone numbers as links automatically.
2) More involved solution:
There is a convenient method to determine the size any text will need to be drawn:
CGSize size = [label.text sizeWithFont:label.font
constrainedToSize:CGSizeMake(label.frame.size.width, CGFLOAT_MAX)
lineBreakMode:UILineBreakModeWordWrap];
You could now determine which field is the phone number by splitting the string into its lines with:
NSArray *comp = [label.text componentsSeparatedByString:#"\n"];
and then checking which one is numeric. Now you would have to calculate the exact frame from the height of your size variable, maybe like this:
CGFloat positionOfNumber; // the index of your line in comp cast to CGFloat
CGFloat buffer = 10; // fiddle with this
CGFloat buttonHeight = (size.height- 2*buffer)/[comp length];
CGFloat buttonY = buffer + positionOfNumber * buttonHeight;
CGRect buttonFrame = CGRectMake(0, buttonY, label.frame.size.width, buttonHeight);
For those who want perfect solution here's the solution on how to get CGRect of a substring.
A subquestion is:
How do I determine what the built-in internal margins of a UITextview are?
I have a long master string of text that I am trying to split into separate UITextView pages that I can then scroll from page to page inside a UIScrollView. I use the following method to determine what the height of a string in a UITextView is and whether the string is over the height limit:
-(NSNumber *)getHeightByWidth: (NSString *) myString
mySize: (UIFont *) mySize
myWidth: (NSNumber *) myWidth
{
int intMyWidth = [myWidth intValue];
CGSize boundingSize = CGSizeMake(intMyWidth, CGFLOAT_MAX);
CGSize requiredSize = [myString sizeWithFont:mySize constrainedToSize:boundingSize lineBreakMode:UILineBreakModeWordWrap];
NSNumber *retNumber = [[NSNumber alloc] initWithFloat:requiredSize.height];
return retNumber;
[retNumber release];
}
I call the getHeightByWidth method using the following cellFont as the input for mySize:
UIFont *cellFont = [UIFont fontWithName:#"Arial" size:14.0];
The UITextView is 320 pixels wide, but I notice that the text doesn't go from the left edge to the right edge as there are internal margins which look to be around 10 pixels on each side. So when I call getHeightByWidth I set myWidth = (320 - 10 - 10); But after building strings to fit within the UITextView, there are usually gaps on the last row that could be filled with the next words in the master string.
Can anyone tell me why these gaps on the last row of the text occur using this process for UITextView?
The built-in margins are represented by the property contentInset.
Also you can configure the margins yourself.
If you have your text view in IB, look for Content insets. The values must be 0, but it still displays some margin. Trying setting them to negative values such as -4 or -8.
In the code, do something like-
myTextView.contentInset = UIEdgeInsetsMake(-4,-8,0,0);
You have to set these values according to what you find suitable.
I am trying to work out the size of a size of a textView up to the cursor by trimming all the text after the cursor, and then using NSString's sizeWithFont method, like so:
NSString *string = [myTextView.text substringToIndex:myTextView.selectedRange.location];
CGSize size = [string sizeWithFont:myTextView.font constrainedToSize:myTextView.frame.size lineBreakMode:UILineBreakModeWordWrap];
Unfortunately, this never returns quite the right size, probably because the text has margins, so its actual width is less than UITextView's width (thanks to the answerers of this question for working that out).
So I need to work out the size of the margins, and subtract that from the UITextView's size to get the actual size of the text area. Does anyone know how to do that?
Unfortunately it looks like the answer is that there is no margin in UITextView - you just have to simulate one by putting a view behind it, and making the UITextView narrower. If you need the background to scroll with the text, you can listen for scrollViewDidScroll:.
I'd suggest
CGSize tSize = myTextView.frame.size;
tSize.width -= 2 * myTextView.contentInset.left;
tSize.height -= 2 * myTextView.contentInset.top;
CGSize size = [string sizeWithFont:myTextView.font constrainedToSize:tSize lineBreakMode:UILineBreakModeWordWrap];
How can I know that the text is out of fit (that the text is need to be scrolled)?
Does it have any methods or something to do this?
Thanks.
Use the NSString UIKit additions. They allow to calculate the height of a string given the size and font. So you can calculate the height in this way:
CGFloat textViewWidth = CGRectGetWidth(textView.bounds);
CGSize inset = CGSizeMake(5.0,5.0);
CGSize stringSize = [myString sizeWithFont:textView.font constrainedToSize:CGSizeMake(textViewWidth-2*inset.width,1024) lineBreakMode:UILineBreakModeWordWrap];
BOOL textOutOfFit = stringSize.height+2*inset.height>CGRectGetHeight(textView.bounds);
Note that this code requires some fine tuning. Infact text inside text views has some internal margin (that I took into account using the inset structure), so the required text view height will be higher than the calculated string height.
What this code does is to ask NSString to calculate its size when horizontally constrained in the textview boundaries (while 1024 in the height is the maximum UIView height possible).
Then what I do is to check if the returned string height is inside or not the text view boundaries.
Is there a way to get the correct size of an NSString using:
- (CGSize)sizeWithFont:(UIFont *)font forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode
that doesnt get thrown off by 2 or 3 hundred character strings. At the moment if I try to use this method on these long strings it incorrectly calculates them and I end up with lots of whitespace at the bottom of the UITextView.
I've tried using UILineBreakModeWordWrap and UILineBreakModeCharacterWrap.
the resizing is being done in
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat result = 44.0f;
NSString* text = nil;
CGFloat width = 0;
CGFloat tableViewWidth;
CGRect bounds = [UIScreen mainScreen].bounds;
tableViewWidth = bounds.size.width;
width = tableViewWidth - 150;
text = stringWithLongWords;
if (text) {
CGSize textSize = { width, 20000.0f };
CGSize size = [text sizeWithFont:[UIFont systemFontOfSize:10.0f] constrainedToSize:textSize lineBreakMode:UILineBreakModeWordWrap];
size.height += 50.0f;
result = MAX(size.height, 44.0f+30.0f);
}
return result;
}
UITextView is not exactly like a UILabel wrapped in a UIScrollView. It has line spacing different from the font size and margins that sizeWithFont:constrainedToSize:linkBreakMode: doesn't account for.
Knowing your font size you might be able to calculate the # of lines and take line spacing into account. You can guess at the margins and try to trick sizeWithFont: to give a more useful answer.
The popular solutions seem to be:
just use a UILabel if you don't need any UITextView functionality
if you need hyperlinks, overlay UIButtons that look like hyperlinks over a UILabel
use an off-screen UITextView and its sizeToFit method to get a real answer
I had no luck w/ the 3rd option but it sounds like it should work, so perhaps I did something wrong.
I'm going to try using a UILabel and overlaying buttons for hyperlinks. We'll see how that turns out.
If that fails, there is always the option taken by Loren Brichter (of Tweetie fame): draw everything into a UIView yourself using CoreGraphics.
Good luck!
Check out this post How do I size a UITextView to its content?
It looks like textView.contentSize.height should work (with the caveat that the the correct contentSize is only available after the UITextView has been added to the view with addSubview)
You said that you have a UITableView with differing heights. Have you set the reuse identifier to the same thing for all of the cells? It could be that older cells with their height already set are being reused. If this is the problem, you should resize the cell again when it's being reused.
The best solution I have found so far is to have a separate hidden UITextView with the same font settings, and set its text. After that its contetSize should be accurate.
The width you are using is the width for your UITextView... but you aren't concerned with that width, you are concerned with the width of the actual text area nested inside the text view.
UITextViews, by default, have padding around their borders to produce a space in-between the typed text and the edge of the UITextView a few pixels wide (and long for the top)... To get the correct size you shouldn't use
textView.frame.size.width
but rather,
textView.frame.size.width-(textView.contentInset.left+textView.contentInset.right+textView.textContainerInset.left+textView.textContainerInset.right+textView.textContainer.lineFragmentPadding/*left*/+textView.textContainer.lineFragmentPadding/*right*/)
^Which takes the width of the UITextView and subtracts out all the padding so you are left with the width of just the type-able text area.
Same goes for height except for lineFragmentPadding doesn't have a bottom so you only subtract it out once instead of twice.
The final code is something like this:
CGSize textViewContentSize = CGSizeMake(theTextView.frame.size.width-(theTextView.contentInset.left+theTextView.contentInset.right+theTextView.textContainerInset.left+theTextView.textContainerInset.right+theTextView.textContainer.lineFragmentPadding/*left*/+theTextView.textContainer.lineFragmentPadding/*right*/), theTextView.frame.size.height-(theTextView.contentInset.top+theTextView.contentInset.bottom+theTextView.textContainerInset.top+theTextView.textContainerInset.bottom+theTextView.textContainer.lineFragmentPadding/*top*//*+theTextView.textContainer.lineFragmentPadding*//*there is no bottom padding*/));
CGSize calculatedSize = [theTextView.text sizeWithFont:theTextView.font
constrainedToSize:textViewContentSize
lineBreakMode:NSLineBreakByWordWrapping];
CGSize adjustedSize = CGSizeMake(ceilf(calculatedSize.width), ceilf(calculatedSize.height));
Inspired by #MrNickBarker's answer, here's my solution:
CGFloat width = 280.0f;
UITextView *t = [[UITextView alloc] init];
[t setFont:[UIFont systemFontOfSize:17]];
[label setText:#"some short or long text, works both"];
CGRect frame = CGRectMake(0, 0, width, 0);
[t setFrame:frame];
// Here's the trick: after applying the 0-frame, the content size is calculated and can be used in a second invocation
frame = CGRectMake(0, 0, width, t.contentSize.height);
[t setFrame:frame];
The only issue remaining for me is that this doesn't work with modified insets.
Still can't believe such twists are required, but since -[NSString sizeWithFont:forWidth:lineBreakMode:] does not respect insets, paddings, margins, line spacings and the like, it seems this is the only working solution at the moment (i.e. iOS 6).