UISegmentedControl bounds - iphone

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;

Related

Why is UIView drawRect causing first pass of UIScrollview to be choppy/jerky/slow?

EDIT: It looks like the problem code is in a procedural background that I am drawing in a UIView which I am then adding as a subview to UIScrollView. The procedural code is below. It draws box shapes, which look sort of like a skyline. Any Ideas why this is slowing down the first pass of my UIScrollView? It can be as much as a thousand pixels wide or more at times. See image...
- (void)drawRect:(CGRect)rect
{
UIBezierPath *vertLine = [[UIBezierPath alloc] init];
[vertLine moveToPoint:CGPointMake(0,self.frame.size.height)];
int detail = 10;
int ranNum = 0;
int count = self.bounds.size.width/detail;
CGFloat heightIncrement = 0.0;
CGFloat minHeight = self.frame.size.height;
CGFloat xPos = 0;
CGFloat yPos = self.frame.size.height-20;
for (int i =0; i<count; i++)
{
ranNum += (arc4random() % 9)-5;
yPos -= (arc4random() % 30);
[vertLine addLineToPoint:CGPointMake(xPos,yPos)];
xPos += (arc4random() % 20)+10;
[vertLine addLineToPoint:CGPointMake(xPos,yPos)];
yPos += (arc4random() % 30);
[vertLine addLineToPoint:CGPointMake(xPos,yPos)];
xPos += (arc4random() % 30);
[vertLine addLineToPoint:CGPointMake(xPos,yPos)];
if (yPos>self.frame.size.height-10) {
yPos = self.frame.size.height-10;
}
if (yPos<self.frame.size.height-50) {
yPos = self.frame.size.height-50;
}
}
[vertLine addLineToPoint:CGPointMake(count*20,(self.frame.size.height))];
[[UIColor colorWithRed:0.0/255.0 green:38.0/255.0 blue:51.0/255 alpha:1] setFill];
[vertLine fill];
}
I have a jerky scroll view, but ONLY on the first pass. After all the views have been viewed in the scroll view, it is very smooth.
First Pass: During the first pass it appears that when each UIImageView is coming into view (from right to left) There is a jerk right when it is entering the visible area. So if you reference the attached image, you'll see UIImageView 5 entering from right to left. When this happens there is a small pause as if the scroll view is telling the image view to load/prepare to be on stage. I have tried to profile this but I don't see any problems in my code and im not sure how I can profile the methods etc. that I have not overridden. So a sub question would be... What methods are called on a subview of UIScrollView when it is entering the visible area?
As I mention I tried to do the async and other concurrent approaches, but it seems that no matter how the images are loaded, the first pass is always jerky, then its as if the UIScrollView caches the subviews. Is it possible to do this caching/loading up front...
[scrollView cacheSubViews]; I would rather have a slower startup than it to be clunky on the first scroll.
Thanks for any ideas on this or information about how the UIScrollView works with its subviews. I have seen many questions and some solutions about jerky UIScrollViews with UIImageViews as subviews. I have tried many of them, but still have a slow scrollview.
austin
imageWithContentsOfFile is a synchronous process takes a lot of tile.Use some asynchronous way to load the images and the smoothness can be achieved

UICollectionView align logic missing in horizontal paging scrollview

I've got a UICollectionView, which works ok, until I start scrolling.
Here some pics first:
As you can see it's great. As I start scrolling (paging enabled) the first one goes a bit offscreen:
This is the problem. Originaly my view have 3 views and I want to scroll and show 3 views only. But as it scrolls (paging enabled) it hides a little bit of the first view and show little bit of the next first view from the next page.
And here is a video, because it's kinda hard to explain:
Video of the problem (Dropbox)
Here is a picture of my UICollectionView settings:
It's going to be great if someone can help!
The fundamental issue is Flow Layout is not designed to support the paging. To achieve the paging effect, you will have to sacrifice the space between cells. And carefully calculate the cells frame and make it can be divided by the collection view frame without remainders. I will explain the reason.
Saying the following layout is what you wanted.
Notice, the most left margin (green) is not part of the cell spacing. It is determined by the flow layout section inset. Since flow layout doesn't support heterogeneous spacing value. It is not a trivial task.
Therefore, after setting the spacing and inset. The following layout is what you will get.
After scroll to next page. Your cells are obviously not aligned as what you expected.
Making the cell spacing 0 can solve this issue. However, it limits your design if you want the extra margin on the page, especially if the margin is different from the cell spacing. It also requires the view frame must be divisible by the cell frame. Sometimes, it is a pain if your view frame is not fixed (considering the rotation case).
The real solution is to subclass UICollectionViewFlowLayout and override following methods
- (CGSize)collectionViewContentSize
{
// Only support single section for now.
// Only support Horizontal scroll
NSUInteger count = [self.collectionView.dataSource collectionView:self.collectionView
numberOfItemsInSection:0];
CGSize canvasSize = self.collectionView.frame.size;
CGSize contentSize = canvasSize;
if (self.scrollDirection == UICollectionViewScrollDirectionHorizontal)
{
NSUInteger rowCount = (canvasSize.height - self.itemSize.height) / (self.itemSize.height + self.minimumInteritemSpacing) + 1;
NSUInteger columnCount = (canvasSize.width - self.itemSize.width) / (self.itemSize.width + self.minimumLineSpacing) + 1;
NSUInteger page = ceilf((CGFloat)count / (CGFloat)(rowCount * columnCount));
contentSize.width = page * canvasSize.width;
}
return contentSize;
}
- (CGRect)frameForItemAtIndexPath:(NSIndexPath *)indexPath
{
CGSize canvasSize = self.collectionView.frame.size;
NSUInteger rowCount = (canvasSize.height - self.itemSize.height) / (self.itemSize.height + self.minimumInteritemSpacing) + 1;
NSUInteger columnCount = (canvasSize.width - self.itemSize.width) / (self.itemSize.width + self.minimumLineSpacing) + 1;
CGFloat pageMarginX = (canvasSize.width - columnCount * self.itemSize.width - (columnCount > 1 ? (columnCount - 1) * self.minimumLineSpacing : 0)) / 2.0f;
CGFloat pageMarginY = (canvasSize.height - rowCount * self.itemSize.height - (rowCount > 1 ? (rowCount - 1) * self.minimumInteritemSpacing : 0)) / 2.0f;
NSUInteger page = indexPath.row / (rowCount * columnCount);
NSUInteger remainder = indexPath.row - page * (rowCount * columnCount);
NSUInteger row = remainder / columnCount;
NSUInteger column = remainder - row * columnCount;
CGRect cellFrame = CGRectZero;
cellFrame.origin.x = pageMarginX + column * (self.itemSize.width + self.minimumLineSpacing);
cellFrame.origin.y = pageMarginY + row * (self.itemSize.height + self.minimumInteritemSpacing);
cellFrame.size.width = self.itemSize.width;
cellFrame.size.height = self.itemSize.height;
if (self.scrollDirection == UICollectionViewScrollDirectionHorizontal)
{
cellFrame.origin.x += page * canvasSize.width;
}
return cellFrame;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes * attr = [super layoutAttributesForItemAtIndexPath:indexPath];
attr.frame = [self frameForItemAtIndexPath:indexPath];
return attr;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray * originAttrs = [super layoutAttributesForElementsInRect:rect];
NSMutableArray * attrs = [NSMutableArray array];
[originAttrs enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes * attr, NSUInteger idx, BOOL *stop) {
NSIndexPath * idxPath = attr.indexPath;
CGRect itemFrame = [self frameForItemAtIndexPath:idxPath];
if (CGRectIntersectsRect(itemFrame, rect))
{
attr = [self layoutAttributesForItemAtIndexPath:idxPath];
[attrs addObject:attr];
}
}];
return attrs;
}
Notice, above code snippet only supports single section and horizontal scroll direction. But it is not hard to expand.
Also, if you don't have millions of cells. Caching those UICollectionViewLayoutAttributes may be a good idea.
You could disable paging on UICollectionView and implement a custom horizontal scrolling/paging mechanism with a custom page width/offset like this:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
float pageWidth = 210;
float currentOffset = scrollView.contentOffset.x;
float targetOffset = targetContentOffset->x;
float newTargetOffset = 0;
if (targetOffset > currentOffset)
newTargetOffset = ceilf(currentOffset / pageWidth) * pageWidth;
else
newTargetOffset = floorf(currentOffset / pageWidth) * pageWidth;
if (newTargetOffset < 0)
newTargetOffset = 0;
else if (newTargetOffset > scrollView.contentSize.width)
newTargetOffset = scrollView.contentSize.width;
targetContentOffset->x = currentOffset;
[scrollView setContentOffset:CGPointMake(newTargetOffset, 0) animated:YES];
}
This answer is way late, but I have just been playing with this problem and found that the cause of the drift is the line spacing. If you want the UICollectionView/FlowLayout to page at exact multiples of your cells width, you must set:
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *)collectionView.collectionViewLayout;
flowLayout.minimumLineSpacing = 0.0;
You wouldn't think the line spacing comes into play in horizontal scrolling, but apparently it does.
In my case I was experimenting with paging left to right, one cell at a time, with no space between cells. Every turn of the page introduced a tiny bit of drift from the desired position, and it seemed to accumulate linearly. ~10.0 pts per turn. I realized 10.0 is the default value of minimumLineSpacing in the flow layout. When I set it to 0.0, no drift, when I set it to half the bounds width, each page drifted an extra half of the bounds.
Changing the minimumInteritemSpacing had no effect.
edit -- from the documentation for UICollectionViewFlowLayout:
#property (nonatomic) CGFloat minimumLineSpacing;
Discussion
...
For a vertically scrolling grid, this value represents the minimum
spacing between successive rows. For a horizontally scrolling grid,
this value represents the minimum spacing between successive columns.
This spacing is not applied to the space between the header and the
first line or between the last line and the footer.
The default value of this property is 10.0.
The solution from the following article is elegant and simple. The main idea is creation the scrollView on top of your collectionView with passing all contentOffset values.
http://b2cloud.com.au/tutorial/uiscrollview-paging-size/
It should be said by implementing this method:
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity;
I didn't achieve a smooth animation like it's happening with pagingEnabled = YES.
I know this question is old, but for anyone who happens to stumble upon this.
All you have to do to correct this is set the MinimumInterItemSpacing to 0 and decrease the content's frame.
#devdavid was spot on on the flowLayout.minimumLineSpacing to zero.
It can also be done in the layout editor, setting the Min Spacing for Lines to 0:
I think I understand the problem. I'll try and make you understand it too.
If you look closely, then you will see that this issue happens only gradually and not just on the first page swipe.
If I understand correctly, in your app, currently, every UICollectionView item are those rounded boxes which we see, and you have some offset/margin between all of them which is constant. This is what is causing the issue.
Instead, what you should do, is make a UICollectionView item which is 1/3rd of the width of the whole view, and then add that rounded image view inside it. To refer to the image, the green colour should be your UICollectionViewItem and not the black one.
Do you roll your own UICollectionViewFlowLayout?
If so, adding -(CGPoint) targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
withScrollingVelocity:(CGPoint)velocity will help you to calculate where the scrollview should stop.
This might work (NB: UNTESTED!):
-(CGPoint) targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
withScrollingVelocity:(CGPoint)velocity
{
CGFloat offsetAdjustment = MAXFLOAT;
CGFloat targetX = proposedContentOffset.x + self.minimumInteritemSpacing + self.sectionInset.left;
CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
NSArray *array = [super layoutAttributesForElementsInRect:targetRect];
for(UICollectionViewLayoutAttributes *layoutAttributes in array) {
if(layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) {
CGFloat itemX = layoutAttributes.frame.origin.x;
if (ABS(itemX - targetX) < ABS(offsetAdjustment)) {
offsetAdjustment = itemX - targetX;
}
}
}
return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}
My answer is based on answer https://stackoverflow.com/a/27242179/440168 but is more simple.
You should place UIScrollView above UICollectionView and give them equal sizes:
#property (nonatomic, weak) IBOutlet UICollectionView *collectionView;
#property (nonatomic, weak) IBOutlet UIScrollView *scrollView;
Then configure contentInset of collection view, for example:
CGFloat inset = self.view.bounds.size.width*2/9;
self.collectionView.contentInset = UIEdgeInsetsMake(0, inset, 0, inset);
And contentSize of scroll view:
self.scrollView.contentSize = CGSizeMake(self.placesCollectionView.bounds.size.width*[self.collectionView numberOfItemsInSection:0],0);
Do not forget to set delegate of scroll view:
self.scrollView.delegate = self;
And implement main magic:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView == self.scrollView) {
CGFloat inset = self.view.bounds.size.width*2/9;
CGFloat scale = (self.placesCollectionView.bounds.size.width-2*inset)/scrollView.bounds.size.width;
self.collectionView.contentOffset = CGPointMake(scrollView.contentOffset.x*scale - inset, 0);
}
}
your UICollectionView's width should be an exact multiplication of the cell size width + the left and right insets. In your example, if the cell width is 96, then the UICollectionView's width should be (96 + 5 + 5) * 3 = 318.
Or, if you wish to keep UICollectionView's 320 width, your cell size width should be 320 / 3 - 5 - 5 = 96.666.
If this does not help, your UICollectionView's width might be different than what is set in the xib file, when the application runs. To check this - add an NSLog statement to printout the view's size in runtime:
NSLog(#"%#", NSStringFromCGRect(uiContentViewController.view.frame));
This is the same problem that I was experiencing and i posted my solution on another post, so I'll post it again here.
I found a solution to it, and it involved subclassing the UICollectionViewFlowLayout.
My CollectionViewCell size is 302 X 457 and i set my minimum line spacing to be 18 (9pix for each cell)
When you extend from that class there are a few methods that need to be over-ridden. One of them is
(CGSize)collectionViewContentSize
In this method, I needed to add up the total width of what was in the UICollectionView. That includes the ([datasource count] * widthOfCollectionViewCell) + ([datasource count] * 18)
Here is my custom UICollectionViewFlowLayout methods....
-(id)init
{
if((self = [super init])){
self.itemSize = CGSizeMake(302, 457);
self.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
self.minimumInteritemSpacing = 0.0f;
self.minimumLineSpacing = 18.0f;
[self setScrollDirection:UICollectionViewScrollDirectionHorizontal];
}
return self;
}
-(CGSize)collectionViewContentSize{
return CGSizeMake((numCellsCount * 302)+(numCellsCount * 18), 457);
}
This worked for me, so I hope someone else finds it useful!
you also can set view's width to '320+spacing', and then set page enable to yes. it will scroll '320+spacing' for every time. i think it because page enable will scroll view's width but not screen's width.
I think I do have a solution for this issue. But I do not if it's the best.
UICollectionViewFlowLayout does contain a property called sectionInset. So you could set the section Inset to whatever your need is and make 1 page equalling one section. Therefore your scrolling should automatically fit properly in the pages ( = sections)
I had a similar problem with paging. Even though the cell insets were all 0 and the cell was exactly the same size in width and height as the UICollectionView, the paging wasn't proper.
What I noticed sounds like a bug in this version (UICollectionView, iOS 6): I could see that if I worked with a UICollectionView with width = 310px or above, and a height = 538px, I was in trouble. However, if I decreased the width to, say, 300px (same height) I got things working perfectly!
For some future reference, I hope it helps!
I encountered a similar issue when trying to get horizontal paging working on a 3 x 3 grid of cells with section insets and cell & line spacing.
The answer for me (after trying many of the suggestions - including subclassing UICollectionViewFlowLayout and various UIScrollView delegate solutions) was simple. I simply used sections in the UICollectionView by breaking my dataset up into sections of 9 items (or fewer), and utilising the numberOfSectionsInCollectionView and numberOfItemsInSection UICollectionView datasource methods.
The UICollectionView's horizontal paging now works beautifully. I recommend this approach to anyone currently tearing their hair out over a similar scenario.
If you're using the default flow-layout for your UICollectionView and do NOT want any space between each cell, you can set its miniumumLineSpacing property to 0 via:
((UICollectionViewFlowLayout *) self.collectionView.collectionViewLayout).minimumLineSpacing = 0;

iOS/UIFont - reducing font width

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.

How to align baselines of text in UILabels with different font sizes on iOS?

I need to align the baselines of text in UILabels. What I'm currently doing is I'm aligning the baselines of UILabels containing the text, and when the text font size in two labels is different, this results in aligned UILabels baseline but misaligned text baseline (misaligned by a small margin, but still misaligned). The labels are included in a custom UIView subclass, therefore self refers to the encompassing UIView.
here is the relevant code
[self.mySmallLabel sizeToFit];
[self.myBigLabel sizeToFit];
self.mySmallLabel.frame = CGRectMake(0,
self.bounds.size.height - self.mySmallLabel.bounds.size.height,
self.mySmallLabel.bounds.size.width,
self.mySmallLabel.bounds.size.height);
self.myBigLabel.frame = CGRectMake(self.mySmallLabel.frame.origin.x + self.mySmallLabel.bounds.size.width,
self.bounds.size.height - self.myBigLabel.bounds.size.height,
self.myBigLabel.bounds.size.width,
self.myBigLabel.bounds.size.height);
[self.mySmallLabel sizeToFit];
[self.myBigLabel sizeToFit];
This code results in the aligment in the image linked below.
As you can see, even though the UILabel baselines are aligned, the baselines of the text is misaligned by a small margin. How can I align the baselines of text dynamically (because font sizes might change at runtime)?
I was using this answer in a couple of different places, but the baselines were sometimes a pixel off on Retina displays. The snippet below accounts for the screen’s scale:
[majorLabel sizeToFit];
[minorLabel sizeToFit];
CGRect changedFrame = minorLabel.frame;
changedFrame.origin.x = CGRectGetWidth(majorLabel.frame);
const CGFloat scale = [UIScreen mainScreen].scale;
const CGFloat majorLabelBaselineInSuperView = CGRectGetMaxY(majorLabel.frame) + majorLabel.font.descender;
const CGFloat minorLabelBaselineInOwnView = CGRectGetHeight(minorLabel.frame) + minorLabel.font.descender;
changedFrame.origin.y = ceil((majorLabelBaselineInSuperView - minorLabelBaselineInOwnView) * scale) / scale;
minorLabel.frame = changedFrame;
You can get pixel-perfect baseline alignment for any pair of UILabels by using the UIFont ascender value in a simple calculation. Here's how:
[majorLabel sizeToFit];
[minorLabel sizeToFit];
CGRect changedFrame = minorLabel.frame;
changedFrame.origin.y = ceilf(majorLabel.frame.origin.y + (majorLabel.font.ascender - minorLabel.font.ascender));
minorLabel.frame = changedFrame;
ceilf() is used because the font.ascender values may be fractional.
I've tested this on both retina and non-retina devices, with excellent results. Positioning the two labels relative to each other on the x-axis has been omitted, as your needs may vary. If you need a quick explanation of what the UIFont ascender is (plus other UIFont info) check out this clear, concise article.
After iOS9.
With autolayout, UILabel has an anchor called: lastBaselineAnchor.
For example:
hintLabel.lastBaselineAnchor.constraint(equalTo: titleLabel.lastBaselineAnchor).isActive = true
I was looking to do this myself (just now) and found my answer on an almost identical question. It's not simple solution though, we have to do the math.
I only needed to do it with 2 different labels and I'm doing it in a subclass of UIView.
- (void)layoutSubviews {
[majorLabel sizeToFit];
[minorLabel sizeToFit];
CGRect changedFrame = minorLabel.frame;
changedFrame.origin.x = majorLabel.frame.size.width;
changedFrame.origin.y = (majorLabel.frame.size.height + majorLabel.font.descender) - (minorLabel.frame.size.height + minorLabel.font.descender);
minorLabel.frame = changedFrame;
}
With Autolayouts, its much more easier.
Select the 2 labels you wish to align and goto the Align tool. Select "Bottom Edges"/ "Top Edges" / Baseline

How to adjust width of TTStyledTextLabel?

I am implementing an IM app on iOS. I found that three20 library has a TTStyledTextLabel which provides cool features like showing images and url links. However I want to embed the TTStyledTextLabel in a message bubble (just like the sms app shipped with iphone does), where I need the label to adjust its size according to the text length. I found that TTStyledTextLabel can adjust its height according to its width, but I don't know how to make it shrink horizontally when the text is very short and can't fill up a whole line. Any suggestions?
I think I have a slightly better solution: I get the rootFrame of the ttstyledtext and iterate over its sibling frames to find the max width.
It works like this:
TTStyledTextLabel* label = [[TTStyledTextLabel alloc] init];
label.text = [TTStyledText textFromXHTML:myTextToBeDisplayed];
[label sizeToFit];
CGFloat maxWidth = 0;
TTStyledFrame *f = label.text.rootFrame;
while (f) {
int w = f.x + f.width;
if (w > maxWidth) {
maxWidth = w;
}
f = f.nextFrame;
}
return CGSizeMake(maxWidth, label.height);
I tried doing it by incrementally passing the width parameter in size to sizeToFit and looking at the resulting height to give cues in terms of whether the size is ok. But this is not a elegant solution
for (int index = 100; index < 320; index= index+30)
{
label.width = x;
if (label.height < 20)
break;
}