I am working with AsyncDisplayKit (for the first time) and have an ASTextNode inside a ASCellNode. I want to adding padding or an inset around the text inside the ASTextNode. I attempted to wrap it with a ASDisplayNode but whenever I calculated it's size in calculateSizeThatFits: it always returned 0. Any suggestions would be appreciated. The code that is in the ASCellNode subclass is:
- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
{
CGSize textSize = [self.commentNode measure:CGSizeMake(constrainedSize.width - kImageSize - kImageToCommentPadding - kCellPadding - kInnerPadding, constrainedSize.height)];
return CGSizeMake(constrainedSize.width, textSize.height);
}
- (void)layout
{
self.imageNode.frame = CGRectMake(kCellPadding, kCellPadding, kImageSize, kImageSize);
self.imageNode.layer.cornerRadius = kImageSize / 2.f;
self.imageNode.layer.masksToBounds = YES;
self.imageNode.layer.borderColor = [UIColor whiteColor].CGColor;
self.imageNode.layer.borderWidth = 2.f;
self.commentNode.backgroundColor = [UIColor whiteColor];
self.commentNode.layer.cornerRadius = 8.f;
self.commentNode.layer.masksToBounds = YES;
CGSize textSize = self.commentNode.calculatedSize;
self.commentNode.frame = CGRectMake(kCellPadding + kImageSize + kCellPadding, kCellPadding, textSize.width, textSize.height);
}
If you node is returning 0 height / size, you may be forgetting to invalidate its current size after changing its content.
Use: [node invalidateCalculatedSize];
For padding around the text node, you could add a node behind it with the size you want and then set a hitTestSlop on the text node.
This would increase its tappable area.
The better approach might be to embed it inside a custom node e.g.
Interface
#interface InsetTextNode: ASControlNode
#property (nonatomic) UIEdgeInsets textInsets;
#property (nonatomic) NSAttributedString * attributedString;
#property ASTextNode * textNode;
#end
Implementation
#implementation InsetTextNode
- (instancetype)init
{
self = [super init];
if (!self) return nil;
[self addSubnode:textNode];
self.textInsets = UIEdgeInsetsMake(8, 16, 8, 16);
return self;
}
- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
{
CGFloat availableTextWidth = constrainedSize.width - self.textInsets.left - self.textInsets.right;
CGFloat availableTextHeight = constrainedSize.height - self.textInsets.top - self.textInsets.bottom;
CGSize constrainedTextSize = CGSizeMake(availableTextWidth, availableTextHeight);
CGSize textSize = [self.textNode measure:constrainedTextSize];
CGFloat finalWidth = self.textInsets.left + textSize.width + self.textInsets.right;
CGFloat finalHeight = self.textInsets.top + textSize.height + self.textInsets.bottom;
CGSize finalSize = CGSizeMake(finalWidth, finalHeight);
return finalSize;
}
- (void)layout
{
CGFloat textX = self.textInsets.left;
CGFloat textY = self.textInsets.top;
CGSize textSize = self.textNode.calculatedSize;
textNode.frame = CGRectMake(textX, textY, textSize.width, textSize.height);
}
- (NSAttributedString *) attributedString
{
return self.textNode.attributedString;
}
- (void)setAttributedString:(NSAttributedString *)attributedString
{
self.textNode.attributedString = attributedString;
[self invalidateCalculatedSize];
}
- (void)setTextInsets:(UIEdgeInsets)textInsets
{
_textInsets = textInsets;
[self invalidateCalculatedSize];
}
#end
2018: Now it is super easy to achieve the same effect, So just in case someone needs to add padding here is a Swift 4 solution: -
let messageLabel: ASTextNode = {
let messageNode = ASTextNode()
messageNode.textContainerInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
return messageNode
}()
Related
I have a small, single row horizontal layout UICollectionView at the top of the screen. It can contain up to a maximum of 6 items. The problem is that I want all 6 items visible without scrolling (this collection view is also going to be used in a Today extension which doesn't allow scrolling). What I want to do is reduce the cell-size and inter-item spacing a little bit to allow all 6 cells to fit.
Basically I'm trying to avoid this:
I've been playing with this for a while but I'm not sure how to approach it. I created a method that's fired every time an item is added or removed from the collection view, just before [self.collectionview reloadData] is called.
-(void)setupCollectionViewLayout{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout*)self.buttonBarCollectionView.collectionViewLayout;
//Figure out if cells are wider than screen
CGFloat screenwidth = self.view.frame.size.width;
CGFloat sectionInsetLeft = 10;
CGFloat sectionInsetRight = 10;
CGFloat minItemSpacing = flowLayout.minimumInteritemSpacing;
CGSize itemsize = CGSizeMake(58,58);
CGFloat itemsizeWidth = itemsize.width;
CGFloat totalWidth = sectionInsetLeft + sectionInsetRight +
(itemsizeWidth * _armedRemindersArray.count) +
(minItemSpacing * (_armedRemindersArray.count -2));
CGFloat reductionAmount = itemsizeWidth;
if (totalWidth > screenwidth) {
while (totalWidth > screenwidth) {
totalWidth = totalWidth - 1;
reductionAmount = reductionAmount - 1;
}
CGSize newCellSize = CGSizeMake(reductionAmount, reductionAmount);
flowLayout.itemSize = newCellSize;
}
else flowLayout.itemSize = itemsize;
}
This is the result.
Not exactly what I was expecting. Not only did it squash everything to the left and also added a second line, but I also seem to have a cell-reuse issue. Truthfully I would just use static-cells if it was even an option, but unfortunately it seems like it's not possible.
What should I be doing? Subclassing UICollectionViewFlowLayout? Won't that basically do the same thing I'm doing here with the built-in flow layout?
EDIT:
Kujey's answer is definitely closer to what I need. I still have a cell-reuse issue though.
Xcode provides an object designed for your need. It's called UICollectionViewFlowLayout and all you need to do is subclass it and place your cells the way you want. The function prepareForLayout is call every time the collection view needs to update the layout.
The piece of code you need is below :
#import "CustomLayout.h"
#define MainCell #"MainCell"
#interface CustomLayout ()
#property (nonatomic, strong) NSMutableDictionary *layoutInfo;
#end
#implementation CustomLayout
-(NSMutableDictionary *) layoutInfo
{
if (!_layoutInfo) {
_layoutInfo = [NSMutableDictionary dictionary];
}
return _layoutInfo;
}
-(void) prepareLayout
{
NSMutableDictionary *cellLayoutInfo = [NSMutableDictionary dictionary];
NSIndexPath *indexPath;
CGFloat itemWidth;
CGFloat itemSpacing;
CGFloat widthWithoutSpacing = [self collectionViewContentSize].width / ([self.collectionView numberOfItemsInSection:0]);
if (widthWithoutSpacing > [self collectionViewContentSize].height) {
itemWidth = [self collectionViewContentSize].height;
itemSpacing = ([self collectionViewContentSize].width - itemWidth*[self.collectionView numberOfItemsInSection:0])/
([self.collectionView numberOfItemsInSection:0]+1);
}
else {
itemWidth = widthWithoutSpacing;
itemSpacing = 0;
}
CGFloat xPosition = itemSpacing;
for (NSInteger section = 0; section < [self.collectionView numberOfSections]; section++) {
for (NSInteger index = 0 ; index < [self.collectionView numberOfItemsInSection:section] ; index++) {
indexPath = [NSIndexPath indexPathForItem:index inSection:section];
UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGRect currentFrame=itemAttributes.frame;
currentFrame.origin.x = xPosition;
currentFrame.size.width = itemWidth;
currentFrame.size.height = itemWidth;
itemAttributes.frame=currentFrame;
cellLayoutInfo[indexPath] = itemAttributes;
xPosition += itemWidth + itemSpacing;
}
}
self.layoutInfo[MainCell] = cellLayoutInfo;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *allAttributes = [NSMutableArray arrayWithCapacity:self.layoutInfo.count];
[self.layoutInfo enumerateKeysAndObjectsUsingBlock:^(NSString *elementIdentifier, NSDictionary *elementsInfo, BOOL *stop) {
[elementsInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attributes, BOOL *innerStop) {
if (CGRectIntersectsRect(rect, attributes.frame)) {
[allAttributes addObject:attributes];
}
}];
}];
return allAttributes;
}
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
return self.layoutInfo[MainCell][indexPath];
}
-(CGSize) collectionViewContentSize
{
return self.collectionView.frame.size;
}
#end
You can also change the y origin of your cells if you need to center them vertically.
try with this code. I get the width and use _armedRemindersArray (i guess you use this array for the items).
-(void)setupCollectionViewLayout{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout*)self.buttonBarCollectionView.collectionViewLayout;
//Figure out if cells are wider than screen
CGFloat screenwidth = self.view.frame.size.width;
CGFloat width = screenwidth - ((sectionInsetLeft + sectionInsetRight) *_armedRemindersArray.count + minItemSpacing * (_armedRemindersArray.count -2));
CGSize itemsize = CGSizeMake(width,width);
flowLayout.itemSize = itemsize;
}
I don't know why you're setting the itemsize first, and then reducing it. I think you should do it the other way around:
-(void)setupCollectionViewLayout{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout*)self.buttonBarCollectionView.collectionViewLayout;
CGFloat sectionInsetLeft = 10;
CGFloat sectionInsetRight = 10;
CGFloat minItemSpacing = flowLayout.minimumInteritemSpacing;
// Find the appropriate item width (<= 58)
CGFloat screenwidth = self.view.frame.size.width;
CGFloat itemsizeWidth = (screenwidth - sectionInsetLeft - sectionInsetRight - (minItemSpacing * (_armedRemindersArray.count -2))) / _armedRemindersArray.count
itemsizeWidth = itemsizeWidth > 58 ? 58 : itemsizeWidth;
flowLayout.itemSize = CGSizeMake(itemsizeWidth, itemsizeWidth);
}
Does this work? If not, could you please include more of your code?
In this picture with a UITextView saying "Coordinate Geometry", the "e" gets put onto the next line. There is no line space there. I have also tried this with a UILabel (I switched to UITextView to try and fix the problem but there was no difference).
Here is my code for my subclass of UITextView
- (instancetype)initWithFrame:(CGRect)frame text:(NSString *)text circular:(BOOL)isCircular {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor lightGrayColor];//[UIColor clearColor];
self.alpha = 0.5;
self.textColor = [Styles mainTextColour];
[self setFont:[Styles heading2Font]];
[self setTextAlignment:NSTextAlignmentCenter];
self.text = text;
self.editable = NO;
self.contentInset = UIEdgeInsetsMake(-8, 0, -8, 0);
[self resizeFontToFix];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
NSAssert(NO, #"Don't use BubbleText -initWithFrame:, use -initWithFrame: andText: instead");
return nil;
}
- (void)resizeFontToFix {
//Resize to fix
CGFloat fontSize = self.font.pointSize;
CGFloat minSize = 6.0 * [Styles sizeModifier];
while (fontSize > minSize && [self sizeThatFits:(CGSizeMake(self.frame.size.width, FLT_MAX))].height >= self.frame.size.height) {
fontSize -= 1.0;
self.font = [self.font fontWithSize:fontSize];
}
//this below resizes to fit and centres the resulting view
// [self resizeFrameAndCentre];
}
- (void)resizeFrameAndCentre {
CGRect originalFrame = self.frame;
[self sizeToFit];
CGPoint centre = self.center;
centre.x = originalFrame.origin.x + (originalFrame.size.width / 2);
centre.y = originalFrame.origin.y + (originalFrame.size.height / 2);
self.center = centre;
}
I've been working on trying to recreate the stretchy collection view that Evernote uses in iOS 7 and I'm really close to having it working. I've managed to create a custom collection view flow layout that modifies the layout attribute transforms when the content offset y value lies outside collection view bounds. I'm modifying the layout attributes in the layoutAttributesForElementsInRect method and it behaves as expected except that the bottom cells can disappear when you hit the bottom of the scroll view. The further you pull the content offset the more cells can disappear. I think the cells basically get clipped off. It doesn't happen at the top though and I'd expect to see the same behavior in both places. Here's what my flow layout implementation looks like right now.
#implementation CNStretchyCollectionViewFlowLayout
{
BOOL _transformsNeedReset;
CGFloat _scrollResistanceDenominator;
}
- (id)init
{
self = [super init];
if (self)
{
// Set up the flow layout parameters
self.minimumInteritemSpacing = 10;
self.minimumLineSpacing = 10;
self.itemSize = CGSizeMake(320, 44);
self.sectionInset = UIEdgeInsetsMake(10, 0, 10, 0);
// Set up ivars
_transformsNeedReset = NO;
_scrollResistanceDenominator = 800.0f;
}
return self;
}
- (void)prepareLayout
{
[super prepareLayout];
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
// Set up the default attributes using the parent implementation
NSArray *items = [super layoutAttributesForElementsInRect:rect];
// Compute whether we need to adjust the transforms on the cells
CGFloat collectionViewHeight = self.collectionViewContentSize.height;
CGFloat topOffset = 0.0f;
CGFloat bottomOffset = collectionViewHeight - self.collectionView.frame.size.height;
CGFloat yPosition = self.collectionView.contentOffset.y;
// Update the transforms if necessary
if (yPosition < topOffset)
{
// Compute the stretch delta
CGFloat stretchDelta = topOffset - yPosition;
NSLog(#"Stretching Top by: %f", stretchDelta);
// Iterate through all the visible items for the new bounds and update the transform
for (UICollectionViewLayoutAttributes *item in items)
{
CGFloat distanceFromTop = item.center.y;
CGFloat scrollResistance = distanceFromTop / 800.0f;
item.transform = CGAffineTransformMakeTranslation(0, -stretchDelta + (stretchDelta * scrollResistance));
}
// Update the ivar for requiring a reset
_transformsNeedReset = YES;
}
else if (yPosition > bottomOffset)
{
// Compute the stretch delta
CGFloat stretchDelta = yPosition - bottomOffset;
NSLog(#"Stretching bottom by: %f", stretchDelta);
// Iterate through all the visible items for the new bounds and update the transform
for (UICollectionViewLayoutAttributes *item in items)
{
CGFloat distanceFromBottom = collectionViewHeight - item.center.y;
CGFloat scrollResistance = distanceFromBottom / 800.0f;
item.transform = CGAffineTransformMakeTranslation(0, stretchDelta + (-stretchDelta * scrollResistance));
}
// Update the ivar for requiring a reset
_transformsNeedReset = YES;
}
else if (_transformsNeedReset)
{
NSLog(#"Resetting transforms");
_transformsNeedReset = NO;
for (UICollectionViewLayoutAttributes *item in items)
item.transform = CGAffineTransformIdentity;
}
return items;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
// Compute whether we need to adjust the transforms on the cells
CGFloat collectionViewHeight = self.collectionViewContentSize.height;
CGFloat topOffset = 0.0f;
CGFloat bottomOffset = collectionViewHeight - self.collectionView.frame.size.height;
CGFloat yPosition = self.collectionView.contentOffset.y;
// Handle cases where the layout needs to be rebuilt
if (yPosition < topOffset)
return YES;
else if (yPosition > bottomOffset)
return YES;
else if (_transformsNeedReset)
return YES;
return NO;
}
#end
I also zipped up the project for people to try out. Any help would be greatly appreciated as I'm pretty new to creating custom collection view layouts. Here's the link to it:
https://dl.dropboxusercontent.com/u/2975688/StackOverflow/stretchy_collection_view.zip
Thanks everyone!
I was able to solve the problem. I'm not sure if there's actually a bug in iOS or not, but the issue was that the cells were actually getting translated outside the content view of the collection view. Once the cell would get translated far enough, it would get clipped off. I find it interesting that this does not happen in the simulator for non-retina displays, but does with retina displays which is why I feel this may actually be a bug.
With that in mind, a workaround for now is to add padding to the top and bottom of the collection view by overriding the collectionViewContentSize method. Once you do this, if you add padding to the top, you need to adjust the layout attributes for the cells as well so they are in the proper location. The final step is to set the contentInset on the collection view itself to adjust for the padding. Leave the scroll indicator insets alone since those are fine. Here's the implementation of my final collection view controller and the custom flow layout.
CNStretchyCollectionViewController.m
#implementation CNStretchyCollectionViewController
static NSString *CellIdentifier = #"Cell";
- (void)viewDidLoad
{
// Register the cell
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:CellIdentifier];
// Tweak out the content insets
CNStretchyCollectionViewFlowLayout *layout = (CNStretchyCollectionViewFlowLayout *) self.collectionViewLayout;
self.collectionView.contentInset = layout.bufferedContentInsets;
// Set the delegate for the collection view
self.collectionView.delegate = self;
self.collectionView.clipsToBounds = NO;
// Customize the appearance of the collection view
self.collectionView.backgroundColor = [UIColor whiteColor];
self.collectionView.indicatorStyle = UIScrollViewIndicatorStyleDefault;
}
#pragma mark - UICollectionViewDataSource Methods
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 20;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
if ([indexPath row] % 2 == 0)
cell.backgroundColor = [UIColor orangeColor];
else
cell.backgroundColor = [UIColor blueColor];
return cell;
}
#end
CNStretchyCollectionViewFlowLayout.m
#interface CNStretchyCollectionViewFlowLayout ()
- (CGSize)collectionViewContentSizeWithoutOverflow;
#end
#pragma mark -
#implementation CNStretchyCollectionViewFlowLayout
{
BOOL _transformsNeedReset;
CGFloat _scrollResistanceDenominator;
UIEdgeInsets _contentOverflowPadding;
}
- (id)init
{
self = [super init];
if (self)
{
// Set up the flow layout parameters
self.minimumInteritemSpacing = 10;
self.minimumLineSpacing = 10;
self.itemSize = CGSizeMake(320, 44);
self.sectionInset = UIEdgeInsetsMake(10, 0, 10, 0);
// Set up ivars
_transformsNeedReset = NO;
_scrollResistanceDenominator = 800.0f;
_contentOverflowPadding = UIEdgeInsetsMake(100.0f, 0.0f, 100.0f, 0.0f);
_bufferedContentInsets = _contentOverflowPadding;
_bufferedContentInsets.top *= -1;
_bufferedContentInsets.bottom *= -1;
}
return self;
}
- (void)prepareLayout
{
[super prepareLayout];
}
- (CGSize)collectionViewContentSize
{
CGSize contentSize = [super collectionViewContentSize];
contentSize.height += _contentOverflowPadding.top + _contentOverflowPadding.bottom;
return contentSize;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
// Set up the default attributes using the parent implementation (need to adjust the rect to account for buffer spacing)
rect = UIEdgeInsetsInsetRect(rect, _bufferedContentInsets);
NSArray *items = [super layoutAttributesForElementsInRect:rect];
// Shift all the items down due to the content overflow padding
for (UICollectionViewLayoutAttributes *item in items)
{
CGPoint center = item.center;
center.y += _contentOverflowPadding.top;
item.center = center;
}
// Compute whether we need to adjust the transforms on the cells
CGFloat collectionViewHeight = [self collectionViewContentSizeWithoutOverflow].height;
CGFloat topOffset = _contentOverflowPadding.top;
CGFloat bottomOffset = collectionViewHeight - self.collectionView.frame.size.height + _contentOverflowPadding.top;
CGFloat yPosition = self.collectionView.contentOffset.y;
// Update the transforms if necessary
if (yPosition < topOffset)
{
// Compute the stretch delta
CGFloat stretchDelta = topOffset - yPosition;
NSLog(#"Stretching Top by: %f", stretchDelta);
// Iterate through all the visible items for the new bounds and update the transform
for (UICollectionViewLayoutAttributes *item in items)
{
CGFloat distanceFromTop = item.center.y - _contentOverflowPadding.top;
CGFloat scrollResistance = distanceFromTop / _scrollResistanceDenominator;
item.transform = CGAffineTransformMakeTranslation(0, -stretchDelta + (stretchDelta * scrollResistance));
}
// Update the ivar for requiring a reset
_transformsNeedReset = YES;
}
else if (yPosition > bottomOffset)
{
// Compute the stretch delta
CGFloat stretchDelta = yPosition - bottomOffset;
NSLog(#"Stretching bottom by: %f", stretchDelta);
// Iterate through all the visible items for the new bounds and update the transform
for (UICollectionViewLayoutAttributes *item in items)
{
CGFloat distanceFromBottom = collectionViewHeight + _contentOverflowPadding.top - item.center.y;
CGFloat scrollResistance = distanceFromBottom / _scrollResistanceDenominator;
item.transform = CGAffineTransformMakeTranslation(0, stretchDelta + (-stretchDelta * scrollResistance));
}
// Update the ivar for requiring a reset
_transformsNeedReset = YES;
}
else if (_transformsNeedReset)
{
NSLog(#"Resetting transforms");
_transformsNeedReset = NO;
for (UICollectionViewLayoutAttributes *item in items)
item.transform = CGAffineTransformIdentity;
}
return items;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
#pragma mark - Private Methods
- (CGSize)collectionViewContentSizeWithoutOverflow
{
return [super collectionViewContentSize];
}
#end
CNStretchyCollectionViewFlowLayout.h
#interface CNStretchyCollectionViewFlowLayout : UICollectionViewFlowLayout
#property (assign, nonatomic) UIEdgeInsets bufferedContentInsets;
#end
I'm actually going to through this onto Github and I'll post a link to the project once it's up. Thanks again everyone!
I have a UILabel in a UITableView that should be a maximum of 2 lines and have a bit of padding around it (7 left and right and 2 top and bottom). I'm using autolayout and targeting only iOS6 and above. All views are being created and added programatically.
I've subclassed my UILabel, here's the init method:
- (id)init
{
self = [super init];
self.translatesAutoresizingMaskIntoConstraints = NO;
self.numberOfLines = 2;
self.backgroundColor = UIColorFromARGB(0x99000000);
self.textColor = [UIColor whiteColor];
self.font = [UIFont boldSystemFontOfSize:14.0f];
return self;
}
If I add this in, I get the right padding, but it makes it just one line:
- (void)drawTextInRect:(CGRect)rect {
UIEdgeInsets insets = {2, 7, 2, 7};
return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, insets)];
}
I've seen this answer a few times, but it doesn't work for me (no effect):
- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines
{
UIEdgeInsets insets = {2, 7, 2, 7};
return [super textRectForBounds:UIEdgeInsetsInsetRect(bounds,insets) limitedToNumberOfLines:numberOfLines];
}
Does it make a difference that its in a table view? Any help would be appreciated.
As you saw in my above comment, you didn't really say what it's doing that you don't expect. I've just done this myself, and this works with autolayout:
#implementation InsetLabel
- (void) setInsets:(UIEdgeInsets)insets
{
_insets = insets ;
[self invalidateIntrinsicContentSize] ;
}
- (void)drawTextInRect:(CGRect)rect
{
return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, self.insets)];
}
- (void)resizeHeightToFitText
{
CGRect frame = [self bounds];
CGFloat textWidth = frame.size.width - (self.insets.left + self.insets.right);
CGSize newSize = [self.text sizeWithFont:self.font constrainedToSize:CGSizeMake(textWidth, 1000000) lineBreakMode:self.lineBreakMode];
frame.size.height = newSize.height + self.insets.top + self.insets.bottom;
self.frame = frame;
}
- (CGSize) intrinsicContentSize
{
CGSize superSize = [super intrinsicContentSize] ;
superSize.height += self.insets.top + self.insets.bottom ;
superSize.width += self.insets.left + self.insets.right ;
return superSize ;
}
#end
#implementation InsetLabel
- (void) setInsets:(UIEdgeInsets)insets
{
_insets = insets ;
[self invalidateIntrinsicContentSize] ;
}
- (void)drawTextInRect:(CGRect)rect
{
return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, self.insets)];
}
- (void)resizeHeightToFitText
{
CGRect frame = [self frame];
CGFloat textWidth = frame.size.width - (self.insets.left + self.insets.right);
CGSize newSize = [self.text sizeWithFont:self.font constrainedToSize:CGSizeMake(textWidth, 1000000) lineBreakMode:self.lineBreakMode];
frame.size.height = newSize.height + self.insets.top + self.insets.bottom;
self.frame = frame;
}
- (CGSize) intrinsicContentSize
{
CGSize superSize = [super intrinsicContentSize] ;
superSize.height += self.insets.top + self.insets.bottom ;
superSize.width += self.insets.left + self.insets.right ;
return superSize ;
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self resizeHeightToFitText];
}
This works even better imho.
I have a UIView subclass called dpCurvedLabel. It uses CATextLayers to curved text around an arc. It works fine, except that I can't get it perfectly centered in the layers parent view. I want the center point for the arc to be at the very center of the view (even if the view is smaller) so all the text characters are the same distance from the center.
I can get it CLOSE but it's always a at least few pixels off. The amount it's 'off' seems to be effected by the frame size I give each CATextLayer. There's something wrong with the math, but I can't figure out what. My code:
// instantiate a dpCurvedLabel in a super view
dpCurvedLabel *curvedLabel = [[dpCurvedLabel alloc]
initWithFrame:CGRectMake(0, 0, 200, 200) arcSize:360 radius:50
text:#"curve curve curve " font:[UIFont fontWithName:#"HelveticaNeue" size:18]
textColor:[UIColor whiteColor]];
// You can animate a rotation to see a more pronounced effect
// [curvedLabel rotateContinously];
[self addSubview:curvedLabel];
dpCurvedLabel.h
#import <UIKit/UIKit.h>
#interface dpCurvedLabel : UIView
#property CGFloat arcSize;
#property CGFloat radius;
#property (strong) NSString *text;
#property (strong) UIFont *font;
#property (strong) UIColor *textColor;
- (id)initWithFrame:(CGRect)frame arcSize:(CGFloat)arcSize radius:(CGFloat)radius
text:(NSString *)text font:(UIFont *)font
textColor:(UIColor *)textColor;
- (void)rotateContinously;
+ (void)makeCurvedText:(CALayer *)layer arcSize:(CGFloat)arcSize radius:(CGFloat)radius
text:(NSString *)text font:(UIFont *)font textColor:(UIColor *)textColor;
#end
dpCurvedLabel.m
#import "dpCurvedLabel.h"
#implementation dpCurvedLabel
- (id)initWithFrame:(CGRect)frame arcSize:(CGFloat)arcSize radius:(CGFloat)radius text:(NSString *)text font:(UIFont *)font textColor:(UIColor *)textColor
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.opaque = NO;
self.clipsToBounds = NO;
self.arcSize = arcSize;
self.radius = radius;
self.text = text;
self.font = font;
self.textColor = textColor;
}
return self;
}
- (void)layoutSublayersOfLayer:(CALayer *)layer
{
[super layoutSublayersOfLayer:layer];
NSLog(#"laying out sublayers!");
[dpCurvedLabel makeCurvedText:layer arcSize:self.arcSize radius:self.radius text:self.text font:self.font textColor:self.textColor];
}
+ (void)makeCurvedText:(CALayer *)layer arcSize:(CGFloat)arcSize radius:(CGFloat)radius text:(NSString *)text font:(UIFont *)font textColor:(UIColor *)textColor
{
layer.sublayers = nil;
layer.masksToBounds = NO;
CGFloat arcStart = 0;
CGFloat shiftH = 0;
CGFloat shiftV = 0;
BOOL clockwise = YES;
BOOL debugMode = YES;
CGFloat xcenter = CGRectGetMidX(layer.bounds);
CGFloat ycenter = CGRectGetMidY(layer.bounds);
CGFloat angle = arcStart;
CGFloat angleStep = arcSize / text.length;
for ( NSUInteger i = 0; i < text.length; ++i )
{
NSRange range = { .location = i, .length = 1 };
NSString *c = [text substringWithRange:range];
CGFloat yoffset = sin( degreesToRadians(angle) ) * radius;
CGFloat xoffset = cos( degreesToRadians(angle) ) * radius;
CGFloat rotAngle = 90 - angle;
if ( clockwise )
{
yoffset = -yoffset;
rotAngle = -90 + angle;
}
CATextLayer* tl = [[CATextLayer alloc] init];
tl.masksToBounds = NO;
tl.wrapped = NO;
tl.truncationMode = kCATruncationNone;
if ( debugMode )
{
tl.borderWidth = 1;
tl.cornerRadius = 3;
tl.borderColor = [UIColor whiteColor].CGColor;
}
// Text layer frame determined here. Effects how arc is centered.
CGSize charSize = CGSizeMake(20, 20);
tl.frame = CGRectMake( shiftH + xcenter - xoffset, shiftV + ycenter + yoffset, charSize.width, charSize.height );
// *******
tl.font = (__bridge CFTypeRef)(font.fontName);
tl.fontSize = font.pointSize;
tl.foregroundColor = textColor.CGColor;
tl.string = c;
tl.alignmentMode = #"center";
tl.transform = CATransform3DMakeAffineTransform( CGAffineTransformMakeRotation( degreesToRadians(rotAngle) ) );
[layer addSublayer:tl];
angle += angleStep;
}
if ( debugMode )
{
layer.backgroundColor = RGBA(0x00, 0x00, 0x00, .6).CGColor;
}
}
- (void)rotateContinously
{
CABasicAnimation *rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0 /* full rotation*/ * 1 * 1 ];
rotationAnimation.duration = 5;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = INT_MAX;
[self.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}
#end
What's wrong with the math here? Why won't this text arc center itself?
The problem comes from the fact that you are setting the frame of each text layer instead of its position. This means that you are positioning the lower left corner to be at the location where the position should be. Do this instead:
tl.position = CGPointMake( shiftH + xcenter - xoffset, shiftV + ycenter + yoffset);
tl.bounds = CGRectMake(0,0,charSize.width,charSize.height);
And you will find your layers to be exactly where you want them.