UITableViewCell with CAShapeLayer bug with appearing - ios

I have a table view cell with custom backgroundView. This background view have an overriden method:
+ (Class)layerClass
{
return [CAShapeLayer class];
}
In code I specify the path, stroke color, fill color and line width for layer of background view of my cells. Next I try to insert section into table view using method:
- (void)insertSections:(NSIndexSet *)sections
withRowAnimation:(UITableViewRowAnimation)animation;
And at this point I get a bug: layers of inserted cells are displayed only after animation of inserting. That is, during the animation cells have no background and border.
UPDATE:
This is how I set custom background for my cells:
UITableViewCell* cell = /* initializing cell */
CGRect frame = cell.frame;
frame.size.width = tableView.frame.size.width;
frame.origin = CGPointZero;
cell.backgroundView = [[MYCellBackgroundView alloc] initWithFrame:frame];
CAShapeLayer* shapeLayer = (CAShapeLayer *)cell.backgroundView.layer;
CGMutablePathRef path = CGPathCreateMutable();
/* building path with some arcs and lines */
shapeLayer.path = path;
CGPathRelease(path);
shapeLayer.fillColor = FILL_COLOR.CGColor;
shapeLayer.strokeColor = BORDER_COLOR.CGColor;
shapeLayer.lineWidth = LINE_WIDTH;
And I forgot to specify that I use CAShapeLayer only for customizing view of cells, not for animation.

Related

Drawing vertical line in tableview cell between Dynamic Labels with AutoLayout

I have a table view containing two labels which supports multiline and placed vertically in the container. I have fixed the distance between the both label. Texts of Label can be of two or in three lines. I am drawing a line between both the labels using CAShapeLayer and UIBezierPath. My Problem is, I am not getting the actual width of the label, So the vertical line doesn't draw at the desired place. Here is the code I am using for the drawing. What I can think of is my lines are getting drawn first and the text of labels are getting set after that can be the issue. How can I fixed that?
Constraints
leading space from superview
trailing from second label
vertically in container
TableViewCell.m
#implementation KKCDDCell
{
CGFloat borderWidth;
UIBezierPath *borderPath;
}
- (void) awakeFromNib
{
[super awakeFromNib];
/** creating vertical line**/
[self verticalLine];
[self dottedLineWithPath:borderPath withborderWidth:borderWidth];
}
- (void)verticalLine
{
borderPath = [UIBezierPath bezierPath];
[borderPath moveToPoint:CGPointMake(self.labelOne.frame.origin.x +self.labelOne.frame.size.width + 10, self.frame.origin.y + 2)];
[borderPath addLineToPoint:CGPointMake(self.labelOne.frame.origin.x +self.labelOne.frame.size.width + 10, self.frame.origin.y + self.frame.size.height - 2)];
borderWidth = 1.0;
}
- (void)dottedLineWithPath:(UIBezierPath *)path withborderWidth:(CGFloat)lineWidth
{
CAShapeLayer *shapelayer = [CAShapeLayer layer];
shapelayer.strokeStart = 0.0;
shapelayer.strokeColor = [UIColor firstRowColor].CGColor;
shapelayer.lineWidth = borderWidth;
shapelayer.lineJoin = kCALineJoinRound;
shapelayer.lineDashPattern = [NSArray arrayWithObjects:[NSNumber numberWithInt:1],[NSNumber numberWithInt:2 ], nil];
shapelayer.path = path.CGPath;
[self.contentView.layer addSublayer:shapelayer];
}

tableView cell dotted border not rendering properly

Consider me novice in iOS programing. I am trying to show dotted border as the separator as well as a vertical dotted border between the fields, please check the screenshot for detailed view. Till now there is no problem, borders are rendering properly.But at the moment tableView get scrolled up or down, the position of vertical dotted lines are getting changed, please check screenshot. I know this is happening due to the cell reuse property. For all the cell dotted line creating method is same so, I am unable to find the culprit line of code. I am using ViewController and attached an UITableView to it, connected the datasource and delegates. Here is the code for dotted line creation.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
KKCustList *cell = [tableView dequeueReusableCellWithIdentifier:#"custList" forIndexPath:indexPath];
if (cell==nil)
{
cell = [[KKCustList alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"custList"];
}
/** Creating dotted lines for border **/
CAShapeLayer *shapelayer = [CAShapeLayer layer];
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, cell.frame.size.height)];
[path addLineToPoint:CGPointMake(cell.frame.size.width, cell.frame.size.height)];
shapelayer.strokeStart = 0.0;
shapelayer.strokeColor = [UIColor firstRowColor].CGColor;
shapelayer.lineWidth = 2.0;
shapelayer.lineJoin = kCALineJoinRound;
shapelayer.lineDashPattern = [NSArray arrayWithObjects:[NSNumber numberWithInt:1],[NSNumber numberWithInt:2 ], nil];
shapelayer.path = path.CGPath;
[cell.layer addSublayer:shapelayer];
/** creating vertical dotted line **/
CAShapeLayer *shapelayer1 = [CAShapeLayer layer];
UIBezierPath *path1 = [UIBezierPath bezierPath];
[path1 moveToPoint:CGPointMake(cell.soLabel.bounds.origin.x + cell.soLabel.bounds.size.width, cell.soLabel.bounds.origin.y + 10)];
[path1 addLineToPoint:CGPointMake(cell.soLabel.bounds.origin.x + cell.soLabel.bounds.size.width, cell.soLabel.bounds.size.height)];
shapelayer1.strokeStart = 0.0;
shapelayer1.strokeColor = [UIColor firstRowColor].CGColor;
shapelayer1.lineWidth = 1.0;
shapelayer1.lineJoin = kCALineJoinRound;
shapelayer1.lineDashPattern = [NSArray arrayWithObjects:[NSNumber numberWithInt:1],[NSNumber numberWithInt:2 ], nil];
shapelayer1.path = path1.CGPath;
[cell.layer addSublayer:shapelayer1];
UPDATE
moved the code to awakeFromNib
#implementation KKCustList
#synthesize shapelayer1;
- (void)awakeFromNib {
[super awakeFromNib];
/** creating vertical dotted line **/
CAShapeLayer *shapelayer1 = [CAShapeLayer layer];
UIBezierPath *path1 = [UIBezierPath bezierPath];
[path1 moveToPoint:CGPointMake(self.soLabel.bounds.origin.x + self.soLabel.bounds.size.width, self.soLabel.bounds.origin.y + 10)];
[path1 addLineToPoint:CGPointMake(self.soLabel.bounds.origin.x + self.soLabel.bounds.size.width, self.soLabel.bounds.size.height)];
shapelayer1.strokeStart = 0.0;
shapelayer1.strokeColor = [UIColor firstRowColor].CGColor;
shapelayer1.lineWidth = 1.0;
shapelayer1.lineJoin = kCALineJoinRound;
shapelayer1.lineDashPattern = [NSArray arrayWithObjects:[NSNumber numberWithInt:1],[NSNumber numberWithInt:2 ], nil];
shapelayer1.path = path1.CGPath;
[self.contentView.layer addSublayer:shapelayer1];
}
Your current code has a couple of issues:
You keep adding new layers, you never remove old ones, so each time a cell is reused it'll take more memory and the lines could be in the wrong place
You never move the sublayers when the cell size changes
So you need a couple of different fixes:
Add the lines in your cell in awakeFromNib, so they're just added once in the same way that all your other views are
Override layoutSubviews in your cell and move the layers you've added each time it's called so they don't overlap other views
I'd actually drop the sublayers approach probably and use an image view for the line. If the lines might change quite a lot then I'd likely use a custom view class which has a sublayer (as per your current code) and the resizing / layout logic as described above. The reason for that is so that you can apply auto-layout constraints to the view and it will move in relation to the cell and your other views automatically.
You should also remove
if (cell==nil)
{
cell = [[KKCustList alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"custList"];
}
because it will never be used, a cell will always be created for you.

Removing a custom sublayer from a custom uitableviewcell

I need rounded, variable-height tableview cells.
I use the following code to create the rounded background:
- (void)roundView:(UIView *)view withRadius:(float)radius andColour:(UIColor *)colour
{
view.backgroundColor = [UIColor whiteColor];
CAShapeLayer *layer = [[CAShapeLayer alloc] init];
CGMutablePathRef pathRef = CGPathCreateMutable();
CGRect bounds = CGRectInset(view.bounds, 0.0f, 0.0f);
CGPathAddRoundedRect(pathRef, nil, bounds, radius, radius);
layer.path = pathRef;
CFRelease(pathRef);
layer.fillColor = colour.CGColor;
[view.layer insertSublayer:layer atIndex:0];
}
The problem is that if a tall cell is rounded, when it is reused, the earlier sublayer is still there and although the new (lesser) height is correct, the appearance is that the bottom edge of the box is not rounded. Presumably, the (larger) pre-existing layer is being clipped.
An obvious thought was to remove the sublayer, but I can't a way of doing it. I've tried creating a new cell, without reusing one, but this doesn't seem possible.
Create a custom layer property in your cell class, assign your layer to it and call -removeFromSuperLayer on it in cell's -prepareForReuse.
I recalled somewhere that I'd done this before to a button...
self.buttonCancel.layer.cornerRadius = 10;
self.buttonCancel.clipsToBounds = YES;
and it works here too...
cell.textBlurb.layer.cornerRadius = 10;
cell.textBlurb.clipsToBounds = YES;

How to handle UIView rounded corners (using CAShapeLayer) on rotation

I have a UITableViewCell containing a UIView. I would like to round all the corners of the UIView when the cell is the only row in the section (i.e. not selected in my implementation). If the cell is selected then I would only like to round the top two corners of the UIView. I am attempting to do this in the custom tableviewcell's layoutSubviews.
- (void)layoutSubviews
{
[super layoutSubviews];
self.roundedView.layer.mask = nil;
if (self.sectionSelected) {
self.maskLayer = [CAShapeLayer layer];
self.maskLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.roundedView.bounds byRoundingCorners:(UIRectCornerTopLeft|UIRectCornerTopRight) cornerRadii:CGSizeMake(10, 10)].CGPath;
self.roundedView.layer.mask = self.maskLayer;
} else {
self.maskLayer = [CAShapeLayer layer];
self.maskLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.roundedView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(10, 10)].CGPath;
self.roundedView.layer.mask = self.maskLayer;
}
}
Unfortunately this method does not work all the time. If I have a the cell selected in portrait (self.sectionSelected = YES). Then deselect (self.sectionSelected = NO). Then rotate to landscape and select the cell again. It displays the cell the size it is suppose to be in portrait. So I believe it is still applying the portrait layer mask and not the landscape one. If I scroll it out of view and then back it in, it updates to the appropriate width. Also of note, I call [self.tableView reloadData]; in didRotateFromInterfaceOrientation.
Any ideas for avoiding this issue? Thanks in advance
Resolved by simply adding "self.contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth;"

How to round off one corner of a resizable UIView in IOS?

I'm using this code to round off one corner of my UIView:
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
self.view.bounds byRoundingCorners:(UIRectCornerTopLeft) cornerRadii:
CGSizeMake(10.0, 10.0)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.view.bounds;
maskLayer.path = maskPath.CGPath;
self.view.layer.mask = maskLayer;
self.view.layer.masksToBounds = NO;
This code works, as long as I don't ever resize the view. If I make the view larger, the new area does not appear because it's outside the bounds of the mask layer (this mask layer does not automatically resize itself with the view). I could just make the mask as large as it will ever need to be, but it could be full-screen on the iPad so I'm worried about performance with a mask that big (I'll have more than one of these in my UI). Also, a super-sized mask wouldn't work for the situation where I need the upper right corner (alone) to be rounded off.
Is there a simpler, easier way to achieve this?
Update: here is what I'm trying to achieve: http://i.imgur.com/W2AfRBd.png (the rounded corner I want is circled here in green).
I have achieved a working version of this, using a subclass of UINavigationController and overriding viewDidLayoutSubviews like so:
- (void)viewDidLayoutSubviews {
CGRect rect = self.view.bounds;
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:rect
byRoundingCorners:UIRectCornerTopLeft cornerRadii:CGSizeMake(8.0, 8.0)];
self.maskLayer = [CAShapeLayer layer];
self.maskLayer.frame = rect;
self.maskLayer.path = maskPath.CGPath;
self.view.layer.mask = self.maskLayer;
}
I then instantiate my UINavigationController subclass with my view controller, and then I offset the frame of the nav controller's view by 20px (y) to expose the status bar and leave a 44-px high navigation bar, as shown in the picture.
The code is working, except that it doesn't handle rotation very well at all. When the app rotates, viewDidLayoutSubviews gets called before the rotation and my code creates a mask that fits the view after rotation; this creates an undesirable blockiness to the rotation, where bits that should be hidden are exposed during the rotation. Also, whereas the app's rotation is perfectly smooth without this mask, with the mask being created the rotation becomes noticeably jerky and slow.
The iPad app Evomail also has rounded corners like this, and their app suffers from the same problem.
The problem is, CoreAnimation properties do not animate in UIKit animation blocks. You need to create a separate animation which will have the same curve and duration as the UIKit animation.
I created the mask layer in viewDidLoad. When the view is about to be layout, I only modify the path property of the mask layer.
You do not know the rotation duration inside the layout callback methods, but you do know it right before rotation (and before layout is triggered), so you can keep it there.
The following code works well.
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
//Keep duration for next layout.
_duration = duration;
}
-(void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
UIBezierPath* maskPath = [UIBezierPath bezierPathWithRoundedRect:self.view.bounds byRoundingCorners:UIRectCornerTopLeft cornerRadii:CGSizeMake(10, 10)];
CABasicAnimation* animation;
if(_duration > 0)
{
animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[animation setDuration:_duration];
//Set old value
[animation setFromValue:(id)((CAShapeLayer*)self.view.layer.mask).path];
//Set new value
[animation setToValue:(id)maskPath.CGPath];
}
((CAShapeLayer*)self.view.layer.mask).path = maskPath.CGPath;
if(_duration > 0)
{
[self.view.layer.mask addAnimation:animation forKey:#"path"];
}
//Zero duration for next layout.
_duration = 0;
}
I know this is a pretty hacky way of doing it but couldn't you just add a png over the top of the corner?
Ugly I know, but it won't affect performance, rotation will be fine if its a subview and users won't notice.
Two ideas:
Resize the mask when the view is resized. You don't get automatic resizing of sublayers the way you get automatic resizing of subviews, but you still get an event, so you can do manual resizing of sublayers.
Or... If this a view whose drawing and display you are in charge of, make the rounding of the corner a part of how you draw the view in the first place (by clipping). That is in fact the most efficient approach.
You could subclass the view you are using and override "layoutSubviews"method. This one gets called everytime your view dimensions change.
Even if "self.view"(referenced in your code) is your viewcontroller's view, you can still set this view to a custom class in your storyboard. Here's the modified code for the subclass:
- (void)layoutSubviews {
[super layoutSubviews];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
self.bounds byRoundingCorners:(UIRectCornerTopLeft) cornerRadii:
CGSizeMake(10.0, 10.0)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
self.layer.masksToBounds = NO;
}
I think you should create a custom view that updates itself any time it is needed, which means anytime that setNeedsDisplay is called.
What I'm suggesting is to create a custom UIView subclass to be implemented as follows:
// OneRoundedCornerUIView.h
#import <UIKit/UIKit.h>
#interface OneRoundedCornerUIView : UIView //Subclass of UIView
#end
// OneRoundedCornerUIView.m
#import "OneRoundedCornerUIView.h"
#implementation OneRoundedCornerUIView
- (void) setFrame:(CGRect)frame
{
[super setFrame:frame];
[self setNeedsDisplay];
}
// Override drawRect as follows.
- (void)drawRect:(CGRect)rect
{
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
self.bounds byRoundingCorners:(UIRectCornerTopLeft) cornerRadii:
CGSizeMake(10.0, 10.0)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
self.layer.masksToBounds = NO;
}
#end
Once you've done this you simply need to make your view an OneRoundedCornerUIView instance instead of an UIView one and your view will be updated smoothly every time you resize or change its frame. I've just done some testing and it seems to work perfectly.
This solution can also be easily customised in order to have a view for which you can easily set which corners should be on and which corners should not from your View Controller. Implementation as follows:
// OneRoundedCornerUIView.h
#import <UIKit/UIKit.h>
#interface OneRoundedCornerUIView : UIView //Subclass of UIView
// This properties are declared in the public API so that you can setup from your ViewController (it also works if you decide to add/remove corners at any time as the setter of each of these properties will call setNeedsDisplay - as shown in the implementation file)
#property (nonatomic, getter = isTopLeftCornerOn) BOOL topLeftCornerOn;
#property (nonatomic, getter = isTopRightCornerOn) BOOL topRightCornerOn;
#property (nonatomic, getter = isBottomLeftCornerOn) BOOL bottomLeftCornerOn;
#property (nonatomic, getter = isBottomRightCornerOn) BOOL bottomRightCornerOn;
#end
// OneRoundedCornerUIView.m
#import "OneRoundedCornerUIView.h"
#implementation OneRoundedCornerUIView
- (void) setFrame:(CGRect)frame
{
[super setFrame:frame];
[self setNeedsDisplay];
}
- (void) setTopLeftCornerOn:(BOOL)topLeftCornerOn
{
_topLeftCornerOn = topLeftCornerOn;
[self setNeedsDisplay];
}
- (void) setTopRightCornerOn:(BOOL)topRightCornerOn
{
_topRightCornerOn = topRightCornerOn;
[self setNeedsDisplay];
}
-(void) setBottomLeftCornerOn:(BOOL)bottomLeftCornerOn
{
_bottomLeftCornerOn = bottomLeftCornerOn;
[self setNeedsDisplay];
}
-(void) setBottomRightCornerOn:(BOOL)bottomRightCornerOn
{
_bottomRightCornerOn = bottomRightCornerOn;
[self setNeedsDisplay];
}
// Override drawRect as follows.
- (void)drawRect:(CGRect)rect
{
UIRectCorner topLeftCorner = 0;
UIRectCorner topRightCorner = 0;
UIRectCorner bottomLeftCorner = 0;
UIRectCorner bottomRightCorner = 0;
if (self.isTopLeftCornerOn) topLeftCorner = UIRectCornerTopLeft;
if (self.isTopRightCornerOn) topRightCorner = UIRectCornerTopRight;
if (self.isBottomLeftCornerOn) bottomLeftCorner = UIRectCornerBottomLeft;
if (self.isBottomRightCornerOn) bottomRightCorner = UIRectCornerBottomRight;
UIRectCorner corners = topLeftCorner | topRightCorner | bottomLeftCorner | bottomRightCorner;
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
self.bounds byRoundingCorners:(corners) cornerRadii:
CGSizeMake(10.0, 10.0)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
self.layer.masksToBounds = NO;
}
#end
I'm a fan of doing what #Martin suggests. As long as there isn't animated content behind the rounded-corner then you can pull this off - even with a bitmap image displayed behind the frontmost view needing the rounded corner.
I created a sample project to mimic your screenshot. The magic happens in a UIView subclass called TSRoundedCornerView. You can place this view anywhere you want - above the view you want to show a rounded corner on, set a property to say what corner to round (adjust the radius by adjusting the size of the view), and setting a property that is the "background view" that you want to be visible in the corner.
Here's the repo for the sample: https://github.com/TomSwift/testRoundedCorner
And here's the drawing magic for the TSRoundedCornerView. Basically we create an inverted clip path with our rounded corner, then draw the background.
- (void) drawRect:(CGRect)rect
{
CGContextRef gc = UIGraphicsGetCurrentContext();
CGContextSaveGState(gc);
{
// create an inverted clip path
// (thanks rob mayoff: http://stackoverflow.com/questions/9042725/drawrect-how-do-i-do-an-inverted-clip)
UIBezierPath* bp = [UIBezierPath bezierPathWithRoundedRect: self.bounds
byRoundingCorners: self.corner // e.g. UIRectCornerTopLeft
cornerRadii: self.bounds.size];
CGContextAddPath(gc, bp.CGPath);
CGContextAddRect(gc, CGRectInfinite);
CGContextEOClip(gc);
// self.backgroundView is the view we want to show peering out behind the rounded corner
// this works well enough if there's only one layer to render and not a view hierarchy!
[self.backgroundView.layer renderInContext: gc];
//$ the iOS7 way of rendering the contents of a view. It works, but only if the UIImageView has already painted... I think.
//$ if you try this, be sure to setNeedsDisplay on this view from your view controller's viewDidAppear: method.
// CGRect r = self.backgroundView.bounds;
// r.origin = [self.backgroundView convertPoint: CGPointZero toView: self];
// [self.backgroundView drawViewHierarchyInRect: r
// afterScreenUpdates: YES];
}
CGContextRestoreGState(gc);
}
I thought about this again and I think there is a simpler solution. I updated my sample to showcase both solutions.
The new solution is to simply create a container view that has 4 rounded corners (via CALayer cornerRadius). You can size that view so only the corner you're interested in is visible on screen. This solution doesn't work well if you need 3 corners rounded, or two opposite (on the diagonal) corners rounded. I think it works in most other cases, including the one you've described in your question and screenshot.
Here's the repo for the sample: https://github.com/TomSwift/testRoundedCorner
Try this. Hope this will helps you.
UIView* parent = [[UIView alloc] initWithFrame:CGRectMake(10,10,100,100)];
parent.clipsToBounds = YES;
UIView* child = [[UIView alloc] new];
child.clipsToBounds = YES;
child.layer.cornerRadius = 3.0f;
child.backgroundColor = [UIColor redColor];
child.frame = CGRectOffset(parent.bounds, +4, -4);
[parent addSubView:child];
If you want to do it in Swift I could advice you to use an extension of an UIView. By doing so all subclasses will be able to use the following method:
import QuartzCore
extension UIView {
func roundCorner(corners: UIRectCorner, radius: CGFloat) {
let maskPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSizeMake(radius, radius))
var maskLayer = CAShapeLayer()
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
}
}
self.anImageView.roundCorner(UIRectCorner.TopRight, radius: 10)

Resources