iOS/UIFont - reducing font width - iphone

I have a UILabel that is a fixed size. Unfortunately on rare occasions, the text I need to fit into it doesn't fit! I have tried reducing the font size, but it needs to reduce so much that it looks terrible.
Is it possible to change the font width somehow? UIFont does not seem to have any properties to allow me to do this? Do I need to use a UIWebView and use CSS? I don't know much CSS, so any help is much appreciated if this is the best way to solve this.
Alternatively, any other ways to solve this?
Thanks Craig

The simplest way to shrink just the width of the text is to apply a transform to the label's layer:
label.layer.transform = CATransform3DMakeScale(desiredWidth/textWidth, 1.0, 1.0);

Do you mean you want to squeeze it horizontally while keeping the height? This is achievable, up to about 60% of the regular width. Beyond that it looks terrible.
Here is the drawRect for a UILabel subclass which squeezes independently on either axis if necessary.
// This drawRect for a UILabel subclass reproduces most common UILabel formatting, but does not do truncation, line breaks, or scaling to fit.
// Instead, it identifies cases where the label text is too large on either axis, and shrinks along that axis.
// For small adjustments, this can keep text readable. In extreme cases, it will create an ugly opaque block.
- (void) drawRect:(CGRect)rect;
{
CGRect bounds = [self bounds];
NSString *text = [self text];
UIFont *font = [self font];
// Find the space needed for all the text.
CGSize textSize = [text sizeWithFont:font];
// topLeft is the point from which the text will be drawn. It may have to move due to compensate for scaling, or due to the chosen alignment.
CGPoint topLeft = bounds.origin;
// Default to no scaling.
CGFloat scaleX = 1.0;
CGFloat scaleY = 1.0;
// If the text is too wide for its space, reduce it.
// Remove the second half of this AND statement to have text scale WIDER than normal to fill the space. Useless in most cases, but can be amusing.
if ((textSize.width>0) && (bounds.size.width/textSize.width<1))
{
scaleX = bounds.size.width/textSize.width;
topLeft.x /= scaleX;
}
else
{
// Alignment only matters if the label text doesn't already fill the space available.
switch ([self textAlignment])
{
case UITextAlignmentLeft :
{
topLeft.x = bounds.origin.x;
}
break;
case UITextAlignmentCenter :
{
topLeft.x = bounds.origin.x+(bounds.size.width-textSize.width)/2;
}
break;
case UITextAlignmentRight :
{
topLeft.x = bounds.origin.x+bounds.size.width-textSize.width;
}
break;
}
}
// Also adjust the height if necessary.
if ((textSize.height>0) && (bounds.size.height/textSize.height<1))
{
scaleY = bounds.size.height/textSize.height;
topLeft.y /= scaleY;
}
else
{
// If the label does not fill the height, center it vertically.
// A common feature request is for labels that do top or bottom alignment. If this is needed, add a property for vertical alignment, and obey it here.
topLeft.y = bounds.origin.y+(bounds.size.height-textSize.height)/2;
}
// Having calculated the transformations needed, apply them here.
// All drawing that follows will be scaled.
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextScaleCTM(context, scaleX, scaleY);
// Begin drawing.
// UILabels may have a shadow.
if ([self shadowColor])
{
[[self shadowColor] set];
CGPoint shadowTopLeft = CGPointMake(topLeft.x+[self shadowOffset].width/scaleX, topLeft.y+[self shadowOffset].height/scaleY);
[text drawAtPoint:shadowTopLeft withFont:font];
}
// The text color may change with highlighting.
UIColor *currentTextColor;
if ((![self isHighlighted]) || (![self highlightedTextColor]))
currentTextColor = [self textColor];
else
currentTextColor = [self highlightedTextColor];
// Finally, draw the regular text.
if (currentTextColor)
{
[currentTextColor set];
[text drawAtPoint:topLeft withFont:font];
}
}

You can set the minimum font size of a UILabel to a smaller value, and check Autoshrink to let it automatically shrink. This parameter is available in Interface Builder.
The internal implementation will reduce kerning, which is the width of space between characters. It cannot actually reduce width though.
This is your better bet. If you are still unsatisfied with results. You may have to change your design.

Related

UISegmentedControl bounds

I want to give the following aspect to an UISegmentedControl:
Note the gray background view, and the white background of the segmented control non selected item.
But, if I give a white background to my UISegmentedControl, I get the following:
Note the white square corners around the UISegmentedControl. What should I do to avoid that square corners?
Thank you in advance,
EDIT: If I change the corner radius of the UISegmentedControl's layer, as suggested by onegray, the result is better, but not perfect (note the white line at the right):
Setting the _segmentedControl.layer.cornerRadius = 5; might help.
Update: More complex clip rect to get rid of 1px right space:
CAShapeLayer* mask = [[CAShapeLayer alloc] init];
mask.frame = CGRectMake(0, 0, _segmentedControl.bounds.size.width-1, _segmentedControl.bounds.size.height);
mask.path = [[UIBezierPath bezierPathWithRoundedRect:mask.frame cornerRadius:4] CGPath];
_segmentedControl.layer.mask = mask;
Update: Matthias Bauch provided a good explanation why this whitespace appears on the right side of the UISegmentedControl. So the simplest way to remove it is making segments of fixed size and adjusting them for proper width.
If that should work for all UISegmentedControls it's a bit of a hassle.
The problem is in iOS7 the 1 pt. border between two segments does not count to the size of the segment. E.g. if the frame of your UISegmentedControl is 320 pt. wide you have to remove 1 pt. and than divide by 2.
And (320-1)/2 is 159.5. iOS floors this value down to 159 pt. And you end up with a 1 pt. border and two 159 pt. segments. Which is 319, and not 320. Hence the 1pt. line at the right of your segmentedControl.
There is a way to calculate the "actual" (the size of the rendering on screen) size of the segmentedControl. With that width you can then add a UIView with rounded corners below the UISegmentedControl.
This code should work for all configurations, even if you have manually sized segments in your segmentedControl:
- (UIView *)addBackgroundViewBelowSegmentedControl:(UISegmentedControl *)segmentedControl {
CGFloat autosizedWidth = CGRectGetWidth(segmentedControl.bounds);
autosizedWidth -= (segmentedControl.numberOfSegments - 1); // ignore the 1pt. borders between segments
NSInteger numberOfAutosizedSegmentes = 0;
NSMutableArray *segmentWidths = [NSMutableArray arrayWithCapacity:segmentedControl.numberOfSegments];
for (NSInteger i = 0; i < segmentedControl.numberOfSegments; i++) {
CGFloat width = [segmentedControl widthForSegmentAtIndex:i];
if (width == 0.0f) {
// auto sized
numberOfAutosizedSegmentes++;
[segmentWidths addObject:[NSNull null]];
}
else {
// manually sized
autosizedWidth -= width;
[segmentWidths addObject:#(width)];
}
}
CGFloat autoWidth = floorf(autosizedWidth/(float)numberOfAutosizedSegmentes);
CGFloat realWidth = (segmentedControl.numberOfSegments-1); // add all the 1pt. borders between the segments
for (NSInteger i = 0; i < [segmentWidths count]; i++) {
id width = segmentWidths[i];
if (width == [NSNull null]) {
realWidth += autoWidth;
}
else {
realWidth += [width floatValue];
}
}
CGRect whiteViewFrame = segmentedControl.frame;
whiteViewFrame.size.width = realWidth;
UIView *whiteView = [[UIView alloc] initWithFrame:whiteViewFrame];
whiteView.backgroundColor = [UIColor whiteColor];
whiteView.layer.cornerRadius = 5.0f;
[self.view insertSubview:whiteView belowSubview:segmentedControl];
return whiteView;
}
Please take care of frame changes yourself.
See this screenshot to see the difference between the two controls. All frames are 280 pt. wide.
Because of the formula UISegmentedControl uses the first controls actual size is 278 pt. And the real size of the second one is 279 pt.
The problem is that this somehow relies on the implementation of UISegmentedControl. Apple could for example change the implementation so segmentWidth that end in .5 points will be displayed. They could easily do this on retina displays.
If you use this code you should check your app on new iOS versions as early as possible. We are relying on implementation details, and those could change every day. Fortunately nothing bad happens if they change the implementation. It will just not look good.
I know this is kind of a hack but you could just use a rounded UIView with white background placed just underneath - and aligned with - the segmented control, except for the width which should be equal to the original control's width minus 1.
Result:
Just to clarify Mattias Bauch's excellent answer. You need to set the returned view as a subview to the view (which we call yourMainView) where you have your segmented control:
UIView *segmControlBackground = [self addBackgroundViewBelowSegmentedControl:yourSegmentedControl];
[yourMainView addSubview:segmControlBackground];
And you need to, of course, declare the new method in your header (.h) file:
- (UIView *)addBackgroundViewBelowSegmentedControl:(UISegmentedControl *)segmentedControl;

Right align PlaceHolder text in UITextField

I have a UITextField with the text right-aligned.
I wanted to change the color of the placeholder text, so I use - (void)drawPlaceholderInRect:(CGRect)rect method. It works great BUT the placeholder text is left-aligned now (the text remains right-aligned). I guess I can add some code to override it but I didn't find which one. Thanks in advance !
- (void)drawPlaceholderInRect:(CGRect)rect
{
[[UIColor redColor] setFill];
UIFont *font = [UIFont fontWithName:#"HelveticaNeue-Medium" size:18];
[[self placeholder] drawInRect:rect withFont:font];
}
Here is the code snippet based on Michael solution. You should create subclass of text field and add the below method. Below method basically changes x-position and width of place holder bounds.
- (CGRect)placeholderRectForBounds:(CGRect)bounds{
CGRect newbounds = bounds;
CGSize size = [[self placeholder] sizeWithAttributes:
#{NSFontAttributeName: self.font}];
int width = bounds.size.width - size.width;
newbounds.origin.x = width ;
newbounds.size.width = size.width;
return newbounds;
}
You've discovered that "drawInRect" is automagically drawing from the left edge going right.
What you need to do is adjust the "rect" passed to "drawInRect" to have left edge that allows the right edge of the drawn text to touch the right edge of your UITextField rect.
To do this, I'd recommend using this method: NSString's [self placeholder] sizeWithFont: constrainedToSize:] (assuming [self placeholder] is a NSString) which will give you the true width of the string. Then subtract the width from the right edge of the text field box and you have the left edge where you need to start your drawing from.
I enhanced #Saikiran's snippet a little, this works for me:
- (CGRect)placeholderRectForBounds:(CGRect)bounds
{
return self.editing ? ({CGRect bounds_ = [super placeholderRectForBounds:bounds];
bounds_.origin.x = bounds_.size.width
- ceilf(self.attributedPlaceholder.size.width)
+ self.inset.x;
bounds_.origin.y = .5f * (.5f * bounds_.size.height
- ceilf(self.attributedPlaceholder.size.height));
bounds_.size.width = ceilf(self.attributedPlaceholder.size.width);
bounds_.size.height = ceilf(self.attributedPlaceholder.size.height);
bounds_;
}) : [super placeholderRectForBounds:bounds];
}

UITextView visually changing content position when pasting in text

I have a UITextView which is designed to enlarge to fit the contentView when needed. When I paste in a paragraph of text, however, it puts the start and end points of the content vertically in the wrong places. Entering or deleting a character resets it back to the correct position.
Any ideas why this is?
-(void)textViewDidChange:(UITextView *)textView {
self.textView.frame = CGRectMake(
self.textView.frame.origin.x,
self.textView.frame.origin.y,
self.textView.frame.size.width,
self.textView.contentSize.height + HEADER_ADDITIONAL_HEIGHT);
self.textView.contentOffset = CGPointMake(0, 0);
self.previousContentSize = textView.contentSize;
}
When I used:
textView.contentSize = textView.frame.size;
textView.contentOffset = CGPointZero;
It solved my issue, but created a new issue where we sometimes get weird scrolling while typing or deleting text. So, I used this:
textView.contentSize = CGSizeMake( textView.contentSize.width,
textView.contentSize.height+1);
This also solved the issue. I think what we all need here is the effect which we get whenever the contentSize of a textview is changed. Unfortunately, I do not know what this effect is. If somebody knows, please tell.
Update:
I have found a method which you can use to solve your issue (I used this to resolve mine).
You can ask NSLayoutMAnager to refresh the entire layout:
[textView.textStorage edited:NSTextStorageEditedCharacters range:NSMakeRange(0, textView.textStorage.length) changeInLength:0];
NSLayoutManager attempts to avoid refreshing the layout because it's time consuming and takes a lot of work, so it's set up to only do it when absolutely necessary (lazily).
There are a number of invalidateLayout functions related to this class but none of them cause an actual re-layout when called.
I know this comes late, but I ran into this issue and thought I should share what I came up with in case others find themselves in the same situation.
You are on the right track, but in textViewDidChange: you are missing one important thing: setting the contentSize after updating the frame height.
// I used 0.f for the height, but you can use another value because according to the docs:
// "the actual bounding rectangle returned by this method can be larger
// than the constraints if additional space is needed to render the entire
// string. Typically, the renderer preserves the width constraint and
// adjusts the height constraint as needed."
CGSize size = CGSizeMake(textview.frame.size.width, 0.f);
CGRect rect = [string boundingRectWithSize:size
options:OptionsYouNeedIfAny // NSStringDrawingOptions
context:nil];
// Where MinTextViewHeight is the smallest height for a textView that
// your design can handle
CGFloat height = MAX(ceilf(rect.size.height), MinTextViewHeight);
CGRect rect = textView.frame;
rect.size.height = height;
textView.frame = rect;
// Adjusting the textView contentSize after updating the frame height is one of the things you were missing
textView.contentSize = textView.frame.size;
textView.contentOffset = CGPointZero;
I hope this helps!
See the docs for more info about using boundingRectWithSize:options:context:.

Drawing ruled lines on a UITextView for IPhone

I would like to create a view like the notes app on iPhone and therefor need the view to have ruled lines as per the notes app, I have done this in windows where you need to get the font metrics and then draw the lines onto the device context, has anyone done this in the UITextView if so some help would be appriciated
Subclass UITextView. Override -drawRect:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor);
CGContextSetLineWidth(context, self.lineWidth);
CGFloat strokeOffset = (self.lineWidth / 2);
CGFloat rowHeight = self.font.lineHeight;
if (rowHeight > 0) {
CGRect rowRect = CGRectMake(self.contentOffset.x, - self.bounds.size.height, self.contentSize.width, rowHeight);
while (rowRect.origin.y < (self.bounds.size.height + self.contentSize.height)) {
CGContextMoveToPoint(context, rowRect.origin.x + strokeOffset, rowRect.origin.y + strokeOffset);
CGContextAddLineToPoint(context, rowRect.origin.x + rowRect.size.width + strokeOffset, rowRect.origin.y + strokeOffset);
CGContextDrawPath(context, kCGPathStroke);
rowRect.origin.y += rowHeight;
}
}
}
When you init the text view, be sure to set the contentMode to UIViewContentModeRedraw. Otherwise the lines won't scroll with the text.
self.contentMode = UIViewContentModeRedraw;
This isn't perfect. Ideally you should just draw into the rect that's passed. But I was lazy and this worked for my needs.
I think this works OK but I feel it has been hacked and I do not fully undestand the mechanism of the UITextView class;
first you must add the following to your delegate to force a redraw on scrolling
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// NSLog(#"scrollViewDidScroll The scroll offset is ---%f",scrollView.contentOffset.y);
[noteText setNeedsDisplay];
}
then implement drawRect in the subclass as so
- (void)drawRect:(CGRect)rect {
// Drawing code
// Get the graphics context
CGContextRef ctx = UIGraphicsGetCurrentContext();
[super drawRect:rect];
// Get the height of a single text line
NSString *alpha = #"ABCD";
CGSize textSize = [alpha sizeWithFont:self.font constrainedToSize:self.contentSize lineBreakMode:UILineBreakModeWordWrap];
NSUInteger height = textSize.height;
// Get the height of the view or contents of the view whichever is bigger
textSize = [self.text sizeWithFont:self.font constrainedToSize:self.contentSize lineBreakMode:UILineBreakModeWordWrap];
NSUInteger contentHeight = (rect.size.height > textSize.height) ? (NSUInteger)rect.size.height : textSize.height;
NSUInteger offset = 6 + height; // MAGIC Number 6 to offset from 0 to get first line OK ???
contentHeight += offset;
// Draw ruled lines
CGContextSetRGBStrokeColor(ctx, .8, .8, .8, 1);
for(int i=offset;i < contentHeight;i+=height) {
CGPoint lpoints[2] = { CGPointMake(0, i), CGPointMake(rect.size.width, i) };
CGContextStrokeLineSegments(ctx, lpoints, 2);
}
}
Still worry about this Magic Number 6
Bob
You can try setting the backgroundColor of you textView using an image with ruled lines
textView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"RuledLinesPage.png"]];
Color with pattern image creates a tiled image if the area to be filled with the color is larger than the image. So you will have to make sure that the image size is correct size/tileable (I don't think 'tileable' is a real word but i hope you get what i mean). Also you will have to create the image with ruled lines to best match you textView's font.
Good Luck.
#lukya,
Your solution is bit messy as when we scroll the UITextView the text only scrolls leaving the lines (coming from the image) in its place.
A better solution would be to add subview to your text view where you have drawn the lines. You need to add an observer to the text view in order to track its change in content size as the text increase/decrease.

How to get the size of a NSString

A "quicky": how can I get the size (width) of a NSString?
I'm trying to see if the string width of a string to see if it is bigger than a given width of screen, case in which I have to "crop" it and append it with "...", getting the usual behavior of a UILabel. string.length won't do the trick since AAAAAAAA and iiiiii have the same length but different sizes (for example).
I'm kind of stuck.
Thanks a lot.
This is a different approach. Find out the minimum size of the text so that it won't wrap to more than one line. If it wraps to over one line, you can find out using the height.
You can use this code:
CGSize maximumSize = CGSizeMake(300, 9999);
NSString *myString = #"This is a long string which wraps";
UIFont *myFont = [UIFont fontWithName:#"Helvetica" size:14];
CGSize myStringSize = [myString sizeWithFont:myFont
constrainedToSize:maximumSize
lineBreakMode:self.myLabel.lineBreakMode];
300 is the width of the screen with a little space for margins. You should substitute your own values for font and size, and for the lineBreakMode if you're not using IB.
Now myStringSize will contain a height which you can check against the height of something you know is only 1 line high (using the same font and size). If it's bigger, you'll need to cut the text. Note that you should add a ... to the string before you check it again (adding the ... might push it over the limit again).
Put this code in a loop to cut the text, then check again for the correct height.
Use below method.
Objective-C
- (CGSize)findHeightForText:(NSString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font {
CGSize size = CGSizeZero;
if (text) {
CGRect frame = [text boundingRectWithSize:CGSizeMake(widthValue, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:#{ NSFontAttributeName:font } context:nil];
size = CGSizeMake(frame.size.width, frame.size.height + 1);
}
return size;
}
Swift 3.0
func findHeight(forText text: String, havingWidth widthValue: CGFloat, andFont font: UIFont) -> CGSize {
var size = CGSizeZero
if text {
var frame = text.boundingRect(withSize: CGSize(width: widthValue, height: CGFLOAT_MAX), options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
size = CGSize(width: frame.size.width, height: frame.size.height + 1)
}
return size
}
You need to use Core Graphics to measure the string, as rendered in your specified font and size. See the answers to Measuring the pixel width of a string for a walkthrough.
sizeWithFont:constrainedToSize:lineBreakMode
is deprecated now. Use below code snippet,
UIFont *font=[UIFont fontWithName:#"Arial" size:16.f];
NSString *name = #"APPLE";
CGSize size = [name sizeWithAttributes:#{NSFontAttributeName:font}];
For whatever its worth --- I think the OP takes the wrong way to get there... if the measurement of width only serves to find the place where text should be clipped, and followed by ellipsis --- then OP should be aware of that this facility is implemented in all Text Views in Cocoa...
Pay attention to this enumeration:
typedef NS_ENUM(NSUInteger, NSLineBreakMode) {
NSLineBreakByWordWrapping = 0, // Wrap at word boundaries, default
NSLineBreakByCharWrapping, // Wrap at character boundaries
NSLineBreakByClipping, // Simply clip
NSLineBreakByTruncatingHead, // Truncate at head of line: "...wxyz"
NSLineBreakByTruncatingTail, // Truncate at tail of line: "abcd..."
NSLineBreakByTruncatingMiddle // Truncate middle of line: "ab...yz"
} API_AVAILABLE(macos(10.0), ios(6.0), watchos(2.0), tvos(9.0));
By setting the line breaking mode of your text-field or text view to NSLineBreakByTruncatingTail, you'll achieve what you want, and probably at higher quality, without implementing yourself.