views:

2133

answers:

4

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 minimise her typing.

+1  A: 

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.

Jonathan Arbogast
But I don't think UILabels can be editable, can they? I want the user to be able to enter text in both these fields. (Especially once I've truncated it - the text probably won't make sense.)
Jane Sales
You're right UILabels aren't editable. I guess you've stumped me for now.In general, I'm wondering what the user experience would be when editing your smaller UITextView. Can the user edit all the text or just the portion that hasn't been truncated? If the user is able to edit all the text, how can the user tell that their edits have taken effect if they happen to get truncated once your editing session ends?For these reasons, I might consider keeping the smaller view as a read-only summary of the text.
Jonathan Arbogast
A little more info is needed. I've got a landscape app. What happens is that 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 they choose 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 his photo orientation from portrait to landscape, then I want to truncate the text and then allow him to change it to make sense.
Jane Sales
All I have is another possible alternative for you to consider.I remember when photos were actually things you printed onto paper and held in your hand. In those days, how did people add captions? They wrote on the back of the photo. So maybe you could "flip to the back of the photo" using one of the standard flip animations. Now your UITextView could be bigger and its size could be independent of of photo orientation. You would have more room for displaying the photo itself as well.You could always show the caption with a Label on the front of the photo as well if that's necessary.
Jonathan Arbogast
Thanks for all your help Jonathan - I really appreciate it. The problem is that I do have a card back, but it's used for other things. The text needs to be on the front. I'm quite surprised how difficult this is to do - I would have thought other people would have done it.
Jane Sales
+1  A: 

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

Corey Floyd
+1  A: 

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.

Roger Nolan
Agree, this is a good approach, and so is doing a binary chop on the string length using the method above. I'm still hoping there's a better way, but I get the feeling now there isn't.
Jane Sales
The solution is Courier. Fixed width fonts, no math...
Tristan
@Tristan, only if you ignore whitespace when wrapping. It would be a simpler problem though.
Roger Nolan
+2  A: 

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