I am having class called Circle (a subclass of UIView) and it is defined as IBDesignable, and some of its properties are defined as IBInspectable. Also there are some standard properties on a Circle class that I am trying to set and to see the changes at design time same as I do for inspectable properties. In the case of inspectable properties defined on Circle class, say strokeThickness, if they are changed through IB, everything works and I see the changes immediately on my view / xib.
When I say xib, I mean that I've created MyXib.xib file and dragged UIView on it, and set a custom class to XibOwnerClass. Also, when I load that xib I see changes at design time if change some of inspectable properties of Circle class.
How do I use this:
I have a view controller on a storyboard, and then I drag an UIView and change its class to XibOwnerClass. Imagine XibOwnerClass as a class that has one Circle. Now here, if I have set in my xib file that circle have red stroke, all circles that I add further in a XibOWnerClass will have red stroke, and that's fine. But the thing is, I want to set startAngle and endAngle per circle.
Now even this works, if I override prepareForInterfaceBuilder method and do something like this:
_circle.startAngle = 0.0f;
_circle.endAngle = 360.0f;
_anotherCircle.startAngle = 0.0f;
_circle.endAngle = 270.0f;
But still, if I click on my xib file, I can't see the changes at design time, but rather on runtime. So how to see these changes at design time with this setup?
Here is drawRect: method of a Circle class, but I guess that is not even needed, except it shows the usage of those non-inspectable properties:
In Circle.m file:
#import "Circle.h"
#define getRadians(angle) ((angle) / 180.0 * M_PI)
#define getDegrees(radians) ((radians) * (180.0 / M_PI))
#implementation Circle
- (void)drawRect:(CGRect)rect {
// Drawing code
CGPoint centerPoint = CGPointMake(rect.size.width/2.0f, rect.size.height / 2.0f);
UIBezierPath *path = [UIBezierPath
bezierPathWithArcCenter: centerPoint
radius:(rect.size.height / 2.0f)
startAngle: getRadians(self.startAngle)
endAngle: getRadians(self.endAngle)
clockwise:YES];
CAShapeLayer *shape = [CAShapeLayer new];
shape.path = path.CGPath;
shape.fillColor = self.mainColor.CGColor;
shape.strokeColor = self.strokeColor.CGColor;
shape.lineWidth = self.ringThicknes;
[self.layer addSublayer:shape];
}
And properties are defined in .h as:
#property(nonatomic) CGFloat startAngle;
#property(nonatomic) CGFloat endAngle;
//Some more IBInspectable properties go here, and those are properties that I set when i want to affect all circles.
I have implemented the same and the below solution is working, please make changes in your .h file:
#ifndef IB_DESIGNABLE
#define IB_DESIGNABLE
#endif
#import <UIKit/UIKit.h>
IB_DESIGNABLE #interface Circle : UIView
#property(nonatomic)IBInspectable CGFloat startAngle;
#property(nonatomic)IBInspectable CGFloat endAngle;
#property(nonatomic)IBInspectable CGFloat ringThicknes;
#property(nonatomic)IBInspectable UIColor* mainColor;
#property(nonatomic)IBInspectable UIColor *strokeColor;
#end
And in .m file
#import "Circle.h"
#define getRadians(angle) ((angle) / 180.0 * M_PI)
#define getDegrees(radians) ((radians) * (180.0 / M_PI))
#implementation Circle
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self drawCircle];
}
return self;
}
-(void)drawCircle {
CGPoint centerPoint = CGPointMake(self.frame.size.width/2.0f, self.frame.size.height / 2.0f);
UIBezierPath *path = [UIBezierPath
bezierPathWithArcCenter: centerPoint
radius:(self.frame.size.height / 2.0f)
startAngle: getRadians(self.startAngle)
endAngle: getRadians(self.endAngle)
clockwise:YES];
CAShapeLayer *shape = [CAShapeLayer new];
shape.path = path.CGPath;
shape.fillColor = self.mainColor.CGColor;
shape.strokeColor = self.strokeColor.CGColor;
shape.lineWidth = self.ringThicknes;
[self.layer addSublayer:shape];
}
- (void)drawRect:(CGRect)rect {
// Drawing code
[self drawCircle];
}
If you still face the same issue then make sure to turn on "Automatically Refresh Views" (in: Editor > Automatically Refresh Views) or manually update view by "Refresh All Views" (in: Editor > Refresh All Views) (when you need to update in IB).
Related
The unusual bottom corners of an iPhoneX are Apple's new (2017) "continuous corners for iPhoneX".
It is trivial for any experienced iOS programmer to approximate the curve, but:
Does anyone know exactly how to achieve these, exactly as Apple does?
Even if it's a private call, it would be good to know.
It does seem bizarre that Apple have not explained this.
Please note that it's trivial to "approximate" the curve:
To repeat,
it is trivial for any experienced iOS programmer to approximate the curve.
The question being asked here is specifically how to do Apple actually do it?
Please do not post any more answers showing beginners how to draw a curve and approximate the iPhone curve.
As of iOS 13, there's an API available for this:
https://developer.apple.com/documentation/quartzcore/calayercornercurve
See CALayerCornerCurve.continuous
I wrote an experimental class which constructs a bezier path which overlaps the border of a CALayer due to #Aflah Bhari's comments. The layer has set its private property continuousCornersto YES. This is the result:
The border of the layer is blue while the color of the path is red.
Here is the code. You can set radius and insets in attribute inspector of Interface Builder. I have created the image above by setting the class of the view controllers view to ArcView, its radius to 30.0 and the insets to (20.0, 20.0).
Here is the code:
ArcView.h
IB_DESIGNABLE
#interface ArcView : UIView
#property(nonatomic) IBInspectable CGFloat radius;
#property(nonatomic) IBInspectable CGSize insets;
#end
ArcView.m
#import "ArcView.h"
#interface CALayer(Private)
#property BOOL continuousCorners;
#end
#interface ArcView()
#property (strong) CALayer *borderLayer;
#end
#implementation ArcView
- (void)setRadius:(CGFloat)inRadius {
if(_radius != inRadius) {
_radius = inRadius;
self.borderLayer.cornerRadius = inRadius;
[self setNeedsDisplay];
}
}
- (void)setInsets:(CGSize)inInsets {
if(!CGSizeEqualToSize(_insets, inInsets)) {
_insets = inInsets;
[self setNeedsLayout];
[self setNeedsDisplay];
}
}
- (void)awakeFromNib {
[super awakeFromNib];
self.borderLayer = [CALayer new];
self.borderLayer.borderColor = [[UIColor blueColor] CGColor];
self.borderLayer.borderWidth = 0.5;
self.borderLayer.continuousCorners = YES;
self.borderLayer.cornerRadius = self.radius;
[self.layer addSublayer:self.borderLayer];
}
- (void)layoutSubviews {
[super layoutSubviews];
self.borderLayer.frame = CGRectInset(self.bounds, self.insets.width, self.insets.height);
}
- (void)drawRect:(CGRect)rect {
CGFloat theRadius = self.radius;
CGFloat theOffset = 1.2 * theRadius;
CGRect theRect = CGRectInset(self.bounds, self.insets.width, self.insets.height);
UIBezierPath *thePath = [UIBezierPath new];
CGPoint thePoint;
[thePath moveToPoint:CGPointMake(CGRectGetMinX(theRect) + theOffset, CGRectGetMinY(theRect))];
[thePath addLineToPoint:CGPointMake(CGRectGetMaxX(theRect) - theOffset, CGRectGetMinY(theRect))];
thePoint = CGPointMake(CGRectGetMaxX(theRect), CGRectGetMinY(theRect));
[thePath addQuadCurveToPoint:CGPointMake(CGRectGetMaxX(theRect), CGRectGetMinY(theRect) + theOffset) controlPoint:thePoint];
[thePath addLineToPoint:CGPointMake(CGRectGetMaxX(theRect), CGRectGetMaxY(theRect) - theOffset)];
thePoint = CGPointMake(CGRectGetMaxX(theRect), CGRectGetMaxY(theRect));
[thePath addQuadCurveToPoint:CGPointMake(CGRectGetMaxX(theRect) - theOffset, CGRectGetMaxY(theRect)) controlPoint:thePoint];
[thePath addLineToPoint:CGPointMake(CGRectGetMinX(theRect) + theOffset, CGRectGetMaxY(theRect))];
thePoint = CGPointMake(CGRectGetMinX(theRect), CGRectGetMaxY(theRect));
[thePath addQuadCurveToPoint:CGPointMake(CGRectGetMinX(theRect), CGRectGetMaxY(theRect) - theOffset) controlPoint:thePoint];
[thePath addLineToPoint:CGPointMake(CGRectGetMinX(theRect), CGRectGetMinY(theRect) + theOffset)];
thePoint = CGPointMake(CGRectGetMinX(theRect), CGRectGetMinY(theRect));
[thePath addQuadCurveToPoint:CGPointMake(CGRectGetMinX(theRect) + theOffset, CGRectGetMinY(theRect)) controlPoint:thePoint];
thePath.lineWidth = 0.5;
[[UIColor redColor] set];
[thePath stroke];
}
#end
I hope this helps you with your problem. I've found the factor of 1.2 for theOffset through experiments. You might modify this value if necessary. The value I have chosen for the radius is not optimal and can certainly be improved. But since it depends on the exact distance from the rim, I didn't invest much time for it.
I'm trying to apply an angle gradient to the dashes created with the code I've written inside a custom UIView class, as below. Although it needs tweaking, I'm happy with the results it produces so far.
Given the input parameters in the view initialisation (below), and a frame of 768 * 768 on an iPad Air2 in portrait mode, it produces the following gauge:
First gauge
What I'd like to do is to cause each of the dashes to step through a user-defined gradient, e.g. green to red, much like this (kludged in Photoshop):
Gauge with colours
I've searched high and low, and cannot find anything to achieve this. The only things that come close use different drawing methods, and I want to keep my drawing routine.
As far as I'm concerned, I should simply be able to call:
CGContextSetStrokeColorWithColor(myContext, [gradient color goes here])
inside the draw loop, and that's it, but I don't know how to create the relevant color array/gradient, and change the line drawing color according to an index into that array.
Any help would be much appreciated.
- (void)drawRect:(CGRect)rect {
myContext = UIGraphicsGetCurrentContext();
UIImage *gaugeImage = [self radials:300 andSteps:3 andLineWidth:10.0];
UIImageView *gaugeImageView = [[UIImageView alloc] initWithImage:gaugeImage];
[self addSubview:gaugeImageView];
}
-(UIImage *)radials:(NSInteger)degrees andSteps:(NSInteger)steps andLineWidth:(CGFloat)lineWidth{
UIGraphicsBeginImageContext(self.bounds.size);
myContext = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(myContext, lineWidth);
CGContextSetStrokeColorWithColor(myContext, [[UIColor blackColor] CGColor]);
CGPoint center = CGPointMake(self.bounds.origin.x+(self.bounds.size.width/2), self.bounds.origin.y+(self.bounds.size.height/2));
CGFloat r1 = center.x * 0.87f;
CGFloat r2 = center.x * 0.95f;
CGContextTranslateCTM(myContext, center.x, center.y);
CGContextBeginPath(myContext);
CGFloat offset = 0;
if(degrees < 360){
offset = (360-degrees) / 2;
}
for(int lp = offset + 0 ; lp < offset + degrees+1 ; lp+=steps){
CGFloat theta = lp * (2 * M_PI / 360);
CGContextMoveToPoint(myContext, 0, 0);
r1 = center.x * 0.87f;
if(lp % 10 == 0){
r1 = center.x * 0.81f;
}
CGContextMoveToPoint(myContext, sin(theta) * r1, cos(theta) * r1);
CGContextAddLineToPoint(myContext, sin(theta) * r2, cos(theta) * r2);
CGContextStrokePath(myContext);
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
So, you want something like this:
First, a couple of gentle suggestions:
Don't add subviews inside drawRect:. What if drawRect: gets called a second time, if for example the view's size changes?
Here's what the View Programming Guide for iOS says about implementing drawRect::
The implementation of your drawRect: method should do exactly one thing: draw your content. This method is not the place to be updating your application’s data structures or performing any tasks not related to drawing. It should configure the drawing environment, draw your content, and exit as quickly as possible. And if your drawRect: method might be called frequently, you should do everything you can to optimize your drawing code and draw as little as possible each time the method is called.
If you need to add or remove subviews, you should do that when the view is initialized, or in layoutSubviews at the latest.
There's no need to draw into an image or use an image view at all. The whole point of drawRect: is to draw into the current graphics context, which UIKit has already set up to target the view's backing store.
Those suggestions aside, there is no support for angular gradients in Core Graphics. However, for your graphic, you can set the color for each tick mark separately and get a pretty good approximation, which is how I created the image above. Use +[UIColor colorWithHue:saturation:brightness:alpha:] to create your color, calculating the hue parameter based on the tick angle.
If you factor out the drawing code into a separate class, it's easy to use it to draw either directly to a view (in drawRect:), or to an image if you need to. Here's the interface:
#interface RainbowGaugeAppearance: NSObject
#property (nonatomic) CGFloat startDegrees;
#property (nonatomic) CGFloat endDegrees;
#property (nonatomic) CGFloat degreesPerMajorTick;
#property (nonatomic) int subdivisionsPerMajorTick;
#property (nonatomic) CGFloat tickThickness;
#property (nonatomic) CGFloat startHue;
#property (nonatomic) CGFloat endHue;
#property (nonatomic) CGFloat outerRadiusFraction;
#property (nonatomic) CGFloat minorInnerRadiusFraction;
#property (nonatomic) CGFloat majorInnerRadiusFraction;
- (instancetype _Nonnull)init;
- (void)drawInRect:(CGRect)rect;
#end
And the implementation:
#implementation RainbowGaugeAppearance
static CGFloat radiansForDegrees(CGFloat degrees) { return degrees * M_PI / 180; }
- (instancetype _Nonnull)init {
if (self = [super init]) {
_startDegrees = 120;
_endDegrees = _startDegrees + 300;
_degreesPerMajorTick = 30;
_subdivisionsPerMajorTick = 10;
_tickThickness = 4;
_outerRadiusFraction = 0.95;
_minorInnerRadiusFraction = 0.87;
_majorInnerRadiusFraction = 0.81;
_startHue = 1/ 3.0;
_endHue = 0;
}
return self;
}
- (void)drawInRect:(CGRect)rect {
CGContextRef gc = UIGraphicsGetCurrentContext();
CGContextSaveGState(gc); {
CGContextTranslateCTM(gc, CGRectGetMidX(rect), CGRectGetMidY(rect));
CGContextSetLineWidth(gc, self.tickThickness);
CGContextSetLineCap(gc, kCGLineCapButt);
CGFloat outerRadius = _outerRadiusFraction / 2 * rect.size.width;
CGFloat minorInnerRadius = _minorInnerRadiusFraction / 2 * rect.size.width;
CGFloat majorInnerRadius = _majorInnerRadiusFraction / 2 * rect.size.width;
CGFloat degreesPerTick = _degreesPerMajorTick / _subdivisionsPerMajorTick;
for (int i = 0; ; ++i) {
CGFloat degrees = _startDegrees + i * degreesPerTick;
if (degrees > _endDegrees) { break; }
CGFloat t = (degrees - _startDegrees) / (_endDegrees - _startDegrees);
CGFloat hue = _startHue + t * (_endHue - _startHue);
CGContextSetStrokeColorWithColor(gc, [UIColor colorWithHue:hue saturation:0.8 brightness:1 alpha:1].CGColor);
CGFloat sine = sin(radiansForDegrees(degrees));
CGFloat cosine = cos(radiansForDegrees(degrees));
CGFloat innerRadius = (i % _subdivisionsPerMajorTick == 0) ? majorInnerRadius : minorInnerRadius;
CGContextMoveToPoint(gc, outerRadius * cosine, outerRadius * sine);
CGContextAddLineToPoint(gc, innerRadius * cosine, innerRadius * sine);
CGContextStrokePath(gc);
}
} CGContextRestoreGState(gc);
}
#end
Using it to draw a view is then trivial:
#implementation RainbowGaugeView {
RainbowGaugeAppearance *_appearance;
}
- (RainbowGaugeAppearance *_Nonnull)appearance {
if (_appearance == nil) { _appearance = [[RainbowGaugeAppearance alloc] init]; }
return _appearance;
}
- (void)drawRect:(CGRect)rect {
[self.appearance drawInRect:self.bounds];
}
#end
As far as I'm concerned, I should simply be able to call CGContextSetStrokeColorWithColor
Reality, however, is not interested in "as far as you're concerned". You are describing an angle gradient. The reality is that there is no built-in Core Graphics facility for creating an angle gradient.
However, you can do it easily with a good library such as AngleGradientLayer. It is then a simple matter to draw the angle gradient and use your gauge drawing as a mask.
In that way, I got this — not kludged in Photoshop, but done entirely live, in iOS, using AngleGradientLayer, plus your radials:andSteps:andLineWidth: method just copied and pasted in and used to generate the mask:
Here's the only code I had to write. First, generating the angle gradient layer:
+ (Class)layerClass {
return [AngleGradientLayer class];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
AngleGradientLayer *l = (AngleGradientLayer *)self.layer;
l.colors = [NSArray arrayWithObjects:
(id)[UIColor colorWithRed:1 green:0 blue:0 alpha:1].CGColor,
(id)[UIColor colorWithRed:0 green:1 blue:0 alpha:1].CGColor,
nil];
l.startAngle = M_PI/2.0;
}
return self;
}
Second, the mask (this part is in Swift, but that's irrelevant):
let im = self.v.radials(300, andSteps: 3, andLineWidth: 10)
let iv = UIImageView(image:im)
self.v.mask = iv
I know how to draw the circle according to the offset.
If i drop down the tableView too fast to see the process of the draw,so i want to make it slower.How to make it? Thanks.
This is my circle.m
property progress is the offset (0.0~1.0).
- (void)drawRect:(CGRect)rect {
[WMFontColor888888 setStroke];
CGFloat startAngle = - M_PI * 80 / 180;
CGFloat step = 0.0;
step = 33 * M_PI/18 * self.progress;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2) radius:self.bounds.size.width / 2 - 3 startAngle:startAngle endAngle:startAngle + step clockwise:YES];
path.lineWidth = 1.5;
path.lineCapStyle = kCGLineCapRound;
path.lineJoinStyle = kCGLineJoinRound;
[path stroke];
}
If you dig into the layer level, you can subclass CAShapeLayer and use -[CAShapeLayer strokeStart] and -[CAShapeLayer strokeEnd]. Then, you can simply move your CG code into -drawInContext:
Example:
#implementation MyView
- (Class)layerClass
{
return [MyCircleLayer class];
}
- (void)setProgress:(CGFloat)progress
{
MyCircleLayer *layer = (id)self.layer;
[layer setProgress:progress];
}
#end
#interface MyCircleLayer : CAShapeLayer
#property (nonatomic, assign) CGFloat progress;
#end
#implementation MyCircleLayer
// Vary strokeStart/strokeEnd based on where or how you want to animate the drawing
- (void)setProgress:(CGFloat)progress
{
_progress = progress;
/**
Constantly updating strokeStart/strokeEnd should queue up redraws and
should draw fluidly, but this could be delayed with an interval or via layer animation
*/
[self setStrokeEnd:progress];
}
- (void)drawInContext:(CGContextRef)ctx
{
[WMFontColor888888 setStroke];
CGFloat startAngle = - M_PI * 80 / 180;
CGFloat step = 0.0;
// Draw the full circle
...
[path stroke];
}
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)
I have an UIView with a layermask (smaller than its frame) that can receive touches.
Now the thing is that i want to restrict those touches within the layer mask.
The mask is a rendered shape and not always a rectangle.
Do i have to do this with:
pointInside:withEvent:
or
hitTest:withEvent:
Or is there a better solution?
Its bit old question but maybe it would be useful for someone :)
At your views .h file;
#interface CustomPhotoFrame : UIView {
#protected
CGPathRef borderPath;
}
#property (nonatomic, assign) CGPathRef borderPath;
And, at your .m file;
#synthesize borderPath;
- (void)setClippingPath:(CGPathRef)path
{
CGPathRelease(borderPath);
borderPath = path;
CGPathRetain(borderPath);
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
return CGPathContainsPoint(borderPath, NULL, point, FALSE);
}
At you drawrect method; call;
UIBezierPath *aPath = [UIBezierPath bezierPath];
// your path codes.. assume that its a circle;
aPath = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(centeredCircleRect)];
CGPathRef cgPath = CGPathCreateCopy(aPath.CGPath);
[self setClippingPath:cgPath];
Now your touch method only detects if your touch is in circle masked view.
Solved it by creating a method that checks if there a solid or transparant pixel in the layer at a specific position:
- (BOOL)isSolidPixel:(CGImageRef)image withXPosition:(int)xPos andYPosition:(int)yPos {
if(xPos > CGImageGetWidth(image) || yPos > CGImageGetHeight(image))
return NO;
CFDataRef pixelData = CGDataProviderCopyData(CGImageGetDataProvider(image));
const UInt8* data = CFDataGetBytePtr(pixelData);
int pixelInfo = yPos * CGImageGetBytesPerRow(image) + xPos * 4;
UInt8 alpha = data[pixelInfo];
CFRelease(pixelData);
if (alpha)
return YES;
return NO;
}