Cut Out Shape with Animation - ios

I want to do something similar to the following:
How to mask an image in IOS sdk?
I want to cover the entire screen with translucent black. Then, I want to cut a circle out of the translucent black covering so that you can see through clearly. I'm doing this to highlight parts of the screen for a tutorial.
I then want to animate the cut-out circle to other parts of the screen. I also want to be able to stretch the cut-out circle horizontally & vertically, as you would do with a generic button background image.

(UPDATE: Please see also my other answer which describes how to set up multiple independent, overlapping holes.)
Let's use a plain old UIView with a backgroundColor of translucent black, and give its layer a mask that cuts a hole out of the middle. We'll need an instance variable to reference the hole view:
#implementation ViewController {
UIView *holeView;
}
After loading the main view, we want to add the hole view as a subview:
- (void)viewDidLoad {
[super viewDidLoad];
[self addHoleSubview];
}
Since we want to move the hole around, it will be convenient to make the hole view be very large, so that it covers the rest of the content regardless of where it's positioned. We'll make it 10000x10000. (This doesn't take up any more memory because iOS doesn't automatically allocate a bitmap for the view.)
- (void)addHoleSubview {
holeView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10000, 10000)];
holeView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.5];
holeView.autoresizingMask = 0;
[self.view addSubview:holeView];
[self addMaskToHoleView];
}
Now we need to add the mask that cuts a hole out of the hole view. We'll do this by creating a compound path consisting of a huge rectangle with a smaller circle at its center. We'll fill the path with black, leaving the circle unfilled and therefore transparent. The black part has alpha=1.0 and so it makes the hole view's background color show. The transparent part has alpha=0.0, so that part of the hole view is also transparent.
- (void)addMaskToHoleView {
CGRect bounds = holeView.bounds;
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = bounds;
maskLayer.fillColor = [UIColor blackColor].CGColor;
static CGFloat const kRadius = 100;
CGRect const circleRect = CGRectMake(CGRectGetMidX(bounds) - kRadius,
CGRectGetMidY(bounds) - kRadius,
2 * kRadius, 2 * kRadius);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:circleRect];
[path appendPath:[UIBezierPath bezierPathWithRect:bounds]];
maskLayer.path = path.CGPath;
maskLayer.fillRule = kCAFillRuleEvenOdd;
holeView.layer.mask = maskLayer;
}
Notice that I've put the circle at the center of the 10000x10000 view. This means that we can just set holeView.center to set the center of the circle relative to the other content. So, for example, we can easily animate it up and down over the main view:
- (void)viewDidLayoutSubviews {
CGRect const bounds = self.view.bounds;
holeView.center = CGPointMake(CGRectGetMidX(bounds), 0);
// Defer this because `viewDidLayoutSubviews` can happen inside an
// autorotation animation block, which overrides the duration I set.
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:2 delay:0
options:UIViewAnimationOptionRepeat
| UIViewAnimationOptionAutoreverse
animations:^{
holeView.center = CGPointMake(CGRectGetMidX(bounds),
CGRectGetMaxY(bounds));
} completion:nil];
});
}
Here's what it looks like:
But it's smoother in real life.
You can find a complete working test project in this github repository.

This is not a simple one. I can get you a good bit of the way there. It's the animating that is tricky. Here's the output of some code I threw together:
The code is like this:
- (void)viewDidLoad
{
[super viewDidLoad];
// Create a containing layer and set it contents with an image
CALayer *containerLayer = [CALayer layer];
[containerLayer setBounds:CGRectMake(0.0f, 0.0f, 500.0f, 320.0f)];
[containerLayer setPosition:[[self view] center]];
UIImage *image = [UIImage imageNamed:#"cool"];
[containerLayer setContents:(id)[image CGImage]];
// Create your translucent black layer and set its opacity
CALayer *translucentBlackLayer = [CALayer layer];
[translucentBlackLayer setBounds:[containerLayer bounds]];
[translucentBlackLayer setPosition:
CGPointMake([containerLayer bounds].size.width/2.0f,
[containerLayer bounds].size.height/2.0f)];
[translucentBlackLayer setBackgroundColor:[[UIColor blackColor] CGColor]];
[translucentBlackLayer setOpacity:0.45];
[containerLayer addSublayer:translucentBlackLayer];
// Create a mask layer with a shape layer that has a circle path
CAShapeLayer *maskLayer = [CAShapeLayer layer];
[maskLayer setBorderColor:[[UIColor purpleColor] CGColor]];
[maskLayer setBorderWidth:5.0f];
[maskLayer setBounds:[containerLayer bounds]];
// When you create a path, remember that origin is in upper left hand
// corner, so you have to treat it as if it has an anchor point of 0.0,
// 0.0
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:
CGRectMake([translucentBlackLayer bounds].size.width/2.0f - 100.0f,
[translucentBlackLayer bounds].size.height/2.0f - 100.0f,
200.0f, 200.0f)];
// Append a rectangular path around the mask layer so that
// we can use the even/odd fill rule to invert the mask
[path appendPath:[UIBezierPath bezierPathWithRect:[maskLayer bounds]]];
// Set the path's fill color since layer masks depend on alpha
[maskLayer setFillColor:[[UIColor blackColor] CGColor]];
[maskLayer setPath:[path CGPath]];
// Center the mask layer in the translucent black layer
[maskLayer setPosition:
CGPointMake([translucentBlackLayer bounds].size.width/2.0f,
[translucentBlackLayer bounds].size.height/2.0f)];
// Set the fill rule to even odd
[maskLayer setFillRule:kCAFillRuleEvenOdd];
// Set the translucent black layer's mask property
[translucentBlackLayer setMask:maskLayer];
// Add the container layer to the view so we can see it
[[[self view] layer] addSublayer:containerLayer];
}
You would have to animate the mask layer which you could build up based on user input, but it will be a bit challenging. Notice the lines where I append a rectangular path to the circle path and then set the fill rule a few lines later on the shape layer. These are what make the inverted mask possible. If you leave those out you will instead show the translucent black in the center of the circle and then nothing on the outer part (if that makes sense).
Maybe try to play with this code a bit and see if you can get it animating. I'll play with it some more as I have time, but this is a pretty interesting problem. Would love to see a complete solution.
UPDATE: So here's another stab at it. The trouble here is that this one makes the translucent mask look white instead of black, but the upside is that circle can be animated pretty easily.
This one builds up a composite layer with the translucent layer and the circle layer being siblings inside of a parent layer that gets used as the mask.
I added a basic animation to this one so we could see the circle layer animate.
- (void)viewDidLoad
{
[super viewDidLoad];
CGRect baseRect = CGRectMake(0.0f, 0.0f, 500.0f, 320.0f);
CALayer *containerLayer = [CALayer layer];
[containerLayer setBounds:baseRect];
[containerLayer setPosition:[[self view] center]];
UIImage *image = [UIImage imageNamed:#"cool"];
[containerLayer setContents:(id)[image CGImage]];
CALayer *compositeMaskLayer = [CALayer layer];
[compositeMaskLayer setBounds:baseRect];
[compositeMaskLayer setPosition:CGPointMake([containerLayer bounds].size.width/2.0f, [containerLayer bounds].size.height/2.0f)];
CALayer *translucentLayer = [CALayer layer];
[translucentLayer setBounds:baseRect];
[translucentLayer setBackgroundColor:[[UIColor blackColor] CGColor]];
[translucentLayer setPosition:CGPointMake([containerLayer bounds].size.width/2.0f, [containerLayer bounds].size.height/2.0f)];
[translucentLayer setOpacity:0.35];
[compositeMaskLayer addSublayer:translucentLayer];
CAShapeLayer *circleLayer = [CAShapeLayer layer];
UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0.0f, 0.0f, 200.0f, 200.0f)];
[circleLayer setBounds:CGRectMake(0.0f, 0.0f, 200.0f, 200.0f)];
[circleLayer setPosition:CGPointMake([containerLayer bounds].size.width/2.0f, [containerLayer bounds].size.height/2.0f)];
[circleLayer setPath:[circlePath CGPath]];
[circleLayer setFillColor:[[UIColor blackColor] CGColor]];
[compositeMaskLayer addSublayer:circleLayer];
[containerLayer setMask:compositeMaskLayer];
[[[self view] layer] addSublayer:containerLayer];
CABasicAnimation *posAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
[posAnimation setFromValue:[NSValue valueWithCGPoint:[circleLayer position]]];
[posAnimation setToValue:[NSValue valueWithCGPoint:CGPointMake([circleLayer position].x + 100.0f, [circleLayer position].y + 100)]];
[posAnimation setDuration:1.0f];
[posAnimation setRepeatCount:INFINITY];
[posAnimation setAutoreverses:YES];
[circleLayer addAnimation:posAnimation forKey:#"position"];
}

Here's an answer that works with multiple independent, possibly overlapping spotlights.
I'll set up my view hierarchy like this:
SpotlightsView with black background
UIImageView with `alpha`=.5 (“dim view”)
UIImageView with shape layer mask (“bright view”)
The dim view will appear dimmed because its alpha mixes its image with the black of the top-level view.
The bright view is not dimmed, but it only shows where its mask lets it. So I just set the mask to contain the spotlight areas and nowhere else.
Here's what it looks like:
I'll implement it as a subclass of UIView with this interface:
// SpotlightsView.h
#import <UIKit/UIKit.h>
#interface SpotlightsView : UIView
#property (nonatomic, strong) UIImage *image;
- (void)addDraggableSpotlightWithCenter:(CGPoint)center radius:(CGFloat)radius;
#end
I'll need QuartzCore (also called Core Animation) and the Objective-C runtime to implement it:
// SpotlightsView.m
#import "SpotlightsView.h"
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
I'll need instance variables for the subviews, the mask layer, and an array of individual spotlight paths:
#implementation SpotlightsView {
UIImageView *_dimImageView;
UIImageView *_brightImageView;
CAShapeLayer *_mask;
NSMutableArray *_spotlightPaths;
}
To implement the image property, I just pass it through to your image subviews:
#pragma mark - Public API
- (void)setImage:(UIImage *)image {
_dimImageView.image = image;
_brightImageView.image = image;
}
- (UIImage *)image {
return _dimImageView.image;
}
To add a draggable spotlight, I create a path outlining the spotlight, add it to the array, and flag myself as needing layout:
- (void)addDraggableSpotlightWithCenter:(CGPoint)center radius:(CGFloat)radius {
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(center.x - radius, center.y - radius, 2 * radius, 2 * radius)];
[_spotlightPaths addObject:path];
[self setNeedsLayout];
}
I need to override some methods of UIView to handle initialization and layout. I'll handle being created either programmatically or in a xib or storyboard by delegating the common initialization code to a private method:
#pragma mark - UIView overrides
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
[self commonInit];
}
return self;
}
I'll handle layout in separate helper methods for each subview:
- (void)layoutSubviews {
[super layoutSubviews];
[self layoutDimImageView];
[self layoutBrightImageView];
}
To drag the spotlights when they are touched, I need to override some UIResponder methods. I want to handle each touch separately, so I just loop over the updated touches, passing each one to a helper method:
#pragma mark - UIResponder overrides
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches){
[self touchBegan:touch];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches){
[self touchMoved:touch];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
[self touchEnded:touch];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
[self touchEnded:touch];
}
}
Now I'll implement the private appearance and layout methods.
#pragma mark - Implementation details - appearance/layout
First I'll do the common initialization code. I want to set my background color to black, since that is part of making the dimmed image view dim, and I want to support multiple touches:
- (void)commonInit {
self.backgroundColor = [UIColor blackColor];
self.multipleTouchEnabled = YES;
[self initDimImageView];
[self initBrightImageView];
_spotlightPaths = [NSMutableArray array];
}
My two image subviews will be configured mostly the same way, so I'll call another private method to create the dim image view, then tweak it to actually be dim:
- (void)initDimImageView {
_dimImageView = [self newImageSubview];
_dimImageView.alpha = 0.5;
}
I'll call the same helper method to create the bright view, then add its mask sublayer:
- (void)initBrightImageView {
_brightImageView = [self newImageSubview];
_mask = [CAShapeLayer layer];
_brightImageView.layer.mask = _mask;
}
The helper method that creates both image views sets the content mode and adds the new view as a subview:
- (UIImageView *)newImageSubview {
UIImageView *subview = [[UIImageView alloc] init];
subview.contentMode = UIViewContentModeScaleAspectFill;
[self addSubview:subview];
return subview;
}
To lay out the dim image view, I just need to set its frame to my bounds:
- (void)layoutDimImageView {
_dimImageView.frame = self.bounds;
}
To lay out the bright image view, I need to set its frame to my bounds, and I need to update its mask layer's path to be the union of the individual spotlight paths:
- (void)layoutBrightImageView {
_brightImageView.frame = self.bounds;
UIBezierPath *unionPath = [UIBezierPath bezierPath];
for (UIBezierPath *path in _spotlightPaths) {
[unionPath appendPath:path];
}
_mask.path = unionPath.CGPath;
}
Note that this isn't a true union that encloses each point once. It relies on the fill mode (the default, kCAFillRuleNonZero) to ensure that repeatedly-enclosed points are included in the mask.
Next up, touch handling.
#pragma mark - Implementation details - touch handling
When UIKit sends me a new touch, I'll find the individual spotlight path containing the touch, and attach the path to the touch as an associated object. That means I need an associated object key, which just needs to be some private thing I can take the address of:
static char kSpotlightPathAssociatedObjectKey;
Here I actually find the path and attach it to the touch. If the touch is outside any of my spotlight paths, I ignore it:
- (void)touchBegan:(UITouch *)touch {
UIBezierPath *path = [self firstSpotlightPathContainingTouch:touch];
if (path == nil)
return;
objc_setAssociatedObject(touch, &kSpotlightPathAssociatedObjectKey,
path, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
When UIKit tells me a touch has moved, I see if the touch has a path attached. If so, I translate (slide) the path by the amount that the touch has moved since I last saw it. Then I flag myself for layout:
- (void)touchMoved:(UITouch *)touch {
UIBezierPath *path = objc_getAssociatedObject(touch,
&kSpotlightPathAssociatedObjectKey);
if (path == nil)
return;
CGPoint point = [touch locationInView:self];
CGPoint priorPoint = [touch previousLocationInView:self];
[path applyTransform:CGAffineTransformMakeTranslation(
point.x - priorPoint.x, point.y - priorPoint.y)];
[self setNeedsLayout];
}
I don't actually need to do anything when the touch ends or is cancelled. The Objective-C runtime will de-associated the attached path (if there is one) automatically:
- (void)touchEnded:(UITouch *)touch {
// Nothing to do
}
To find the path that contains a touch, I just loop over the spotlight paths, asking each one if it contains the touch:
- (UIBezierPath *)firstSpotlightPathContainingTouch:(UITouch *)touch {
CGPoint point = [touch locationInView:self];
for (UIBezierPath *path in _spotlightPaths) {
if ([path containsPoint:point])
return path;
}
return nil;
}
#end
I have uploaded a full demo to github.

I've been struggling with this same problem and found some great help here on SO so I thought I'd share my solution combining a few different ideas I found online. One additional feature I added was for the cut-out to have a gradient effect. The added benefit to this solution is that it works with any UIView and not just with images.
First subclass UIView to black out everything except the frames you want cut out:
// BlackOutView.h
#interface BlackOutView : UIView
#property (nonatomic, retain) UIColor *fillColor;
#property (nonatomic, retain) NSArray *framesToCutOut;
#end
// BlackOutView.m
#implementation BlackOutView
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetBlendMode(context, kCGBlendModeDestinationOut);
for (NSValue *value in self.framesToCutOut) {
CGRect pathRect = [value CGRectValue];
UIBezierPath *path = [UIBezierPath bezierPathWithRect:pathRect];
// change to this path for a circular cutout if you don't want a gradient
// UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:pathRect];
[path fill];
}
CGContextSetBlendMode(context, kCGBlendModeNormal);
}
#end
If you don't want the blur effect, then you can swap paths to the oval one and skip the blur mask below. Otherwise, the cutout will be square and filled with a circular gradient.
Create a gradient shape with the center transparent and slowly fading in black:
// BlurFilterMask.h
#interface BlurFilterMask : CAShapeLayer
#property (assign) CGPoint origin;
#property (assign) CGFloat diameter;
#property (assign) CGFloat gradient;
#end
// BlurFilterMask.m
#implementation CRBlurFilterMask
- (void)drawInContext:(CGContextRef)context
{
CGFloat gradientWidth = self.diameter * 0.5f;
CGFloat clearRegionRadius = self.diameter * 0.25f;
CGFloat blurRegionRadius = clearRegionRadius + gradientWidth;
CGColorSpaceRef baseColorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat colors[8] = { 0.0f, 0.0f, 0.0f, 0.0f, // Clear region colour.
0.0f, 0.0f, 0.0f, self.gradient }; // Blur region colour.
CGFloat colorLocations[2] = { 0.0f, 0.4f };
CGGradientRef gradient = CGGradientCreateWithColorComponents (baseColorSpace, colors, colorLocations, 2);
CGContextDrawRadialGradient(context, gradient, self.origin, clearRegionRadius, self.origin, blurRegionRadius, kCGGradientDrawsAfterEndLocation);
CGColorSpaceRelease(baseColorSpace);
CGGradientRelease(gradient);
}
#end
Now you just need to call these two together and pass in the UIViews that you want cutout
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self addMaskInViews:#[self.viewCutout1, self.viewCutout2]];
}
- (void) addMaskInViews:(NSArray *)viewsToCutOut
{
NSMutableArray *frames = [NSMutableArray new];
for (UIView *view in viewsToCutOut) {
view.hidden = YES; // hide the view since we only use their bounds
[frames addObject:[NSValue valueWithCGRect:view.frame]];
}
// Create the overlay passing in the frames we want to cut out
BlackOutView *overlay = [[BlackOutView alloc] initWithFrame:self.view.frame];
overlay.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.8];
overlay.framesToCutOut = frames;
[self.view insertSubview:overlay atIndex:0];
// add a circular gradients inside each view
for (UIView *maskView in viewsToCutOut)
{
BlurFilterMask *blurFilterMask = [BlurFilterMask layer];
blurFilterMask.frame = maskView.frame;
blurFilterMask.gradient = 0.8f;
blurFilterMask.diameter = MIN(maskView.frame.size.width, maskView.frame.size.height);
blurFilterMask.origin = CGPointMake(maskView.frame.size.width / 2, maskView.frame.size.height / 2);
[self.view.layer addSublayer:blurFilterMask];
[blurFilterMask setNeedsDisplay];
}
}

If you just want something that is plug and play, I added a library to CocoaPods that allows you to create overlays with rectangular/circular holes, allowing the user to interact with views behind the overlay. It is a Swift implementation of similar strategies used in other answers. I used it to create this tutorial for one of our apps:
The library is called TAOverlayView, and is open source under Apache 2.0.
Note: I haven't implemented moving holes yet (unless you move the entire overlay as in other answers).

Related

Circular UIButtons Getting Distorted on Rotation Animation Due to Resize

I have this 10 keypad on iPhone that has basically the same screen as the iPhone unlock screen, only mine rotates which resizes the buttons to fit the screen. The problem is that the animation of rotating the device distorts the round shape because they have changed size, but the cornerRadius is the same until it completes the animation. Once the rotation animation has completed, the buttons get the radius set again, thus making them round. I don't know how to have the UIButtons always round, specifically during the rotation animation. Here's basically what I have:
- (void)viewDidLayoutSubviews {
[self setupButtons];
}
- (void)setupButtons {
for (UIButton *button in self.touchPadButtons) {
[self roundifyButton:button withRadius:radius];
}
}
- (void)roundifyButton:(UIButton *)button {
NSInteger height = button.frame.size.height;
radius = height/2;
button.layer.cornerRadius = radius;
button.layer.borderWidth = .6f;
button.layer.borderColor = [UIColor whiteColor].CGColor;
button.clipsToBounds = YES;
}
I've tried using:
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
but it seems like in order for setting the radius to work from that method, I'd have to set the size of my buttons programmatically instead of using autolayout. Does anyone have any magical suggestions on handling this? I would sure love to not rotate, like Apple, but unfortunately that decision was not mine.
Wow, this was tougher than I thought it would be. Fortunately, WWDC is going on right now and I was able to get a solution from the Interface Builder lab. His solution was to subclass UIButton and overwrite the drawRect: method. So this is the only method you should have in the CircleButton class. One issue I found is that the lineWidth property doesn't get set before it's initialized by the nib. I overwrote the init method and set a default value, but it doesn't get hit the first time when the nib initializes the buttons. So I had to add the default value in the drawRect: method. I hope this helps people who need circular UIButtons that can resize.
- (void)drawRect:(CGRect)rect {
[[UIColor whiteColor] set];
if (!self.lineWidth) {
self.lineWidth = 0.75;
}
CGRect bounds = [self bounds];
CGRect circleRect = CGRectMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds), 0, 0);
CGFloat radius = MIN(bounds.size.width, bounds.size.height) / 2.0;
circleRect = CGRectInset(circleRect, -radius, -radius);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(circleRect, self.lineWidth / 2.0, self.lineWidth / 2.0)];
[path setLineWidth:self.lineWidth];
[path stroke];
}
In case you want to animate the button click the way iPhone lock-screen does, you'll need to add this to the CircleButton class. Otherwise only the titleLabel will be highlighted.
- (void)drawRect:(CGRect)rect {
[[UIColor whiteColor] set];
if (!self.lineWidth) {
self.lineWidth = 0.75;
}
CGRect bounds = [self bounds];
CGRect circleRect = CGRectMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds), 0, 0);
CGFloat radius = MIN(bounds.size.width, bounds.size.height) / 2.0;
circleRect = CGRectInset(circleRect, -radius, -radius);
self.layer.cornerRadius = radius;
self.clipsToBounds = YES;
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(circleRect, self.lineWidth / 2.0, self.lineWidth / 2.0)];
[path setLineWidth:self.lineWidth];
[path stroke];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// highlight button on click
self.backgroundColor = [UIColor lightGrayColor];
[super touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
// remove highlight
self.backgroundColor = [UIColor clearColor];
}

Draw Custom shapes (Geofences) zones on MKMapView

I'm creating an Universal app and I have to create custom safe zones on the map view.
What I do is:
Add a new UIView as superview of map view called squareZone.
To the squareZone view I add UIPanGestureRecognizer, UIPinchGestureRecognizer and UIRotationGestureRecognizer so I can move, rotate and zoom (in and out).
Here is the code of SquareZone
- (id)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.opaque = NO;
self.layer.cornerRadius = 5.0f;
}
return self;
}
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
UIColor *color = [UIColor colorWithRed: 0.765 green: 0.439 blue: 0.443 alpha: 0.658];
UIBezierPath *rectanglePath = [UIBezierPath bezierPathWithRect: rect];
[color setFill];
[rectanglePath fill];
[UIColor.whiteColor setStroke];
rectanglePath.lineWidth = 5;
CGFloat rectanglePattern[] = {2, 3};
[rectanglePath setLineDash: rectanglePattern count: 2 phase: 0];
[rectanglePath stroke];
}
Now, when the user adjust the squareZone I have to show on a UILabel the distance between each point in meters. For that task I'm using
- (CLLocationDistance)distanceFromLocation:(const CLLocation *)location
How can I add/show the four UILabels when the user interacts with the squareZone.
I need some light here. I had seen many tutorials but I cannot imagine how can this is posible. For reference, there is an app called
Trax: https://itunes.apple.com/us/app/trax-gps-tracker/id647170688?mt=8
I have to do the same Drawing Geofence Zone.
Thanks in advance.
First thing to mention is that Trax has two different modes:
1) editing -- where you draw the path
2) live -- where it shows the result.
Editing mode
While in editing, you cannot move and zoom inside of your MKMapView. This happens because they are using the same approach as you do -- they are adding a UIView on top of MKMapView, so that gestures do not conflict with each other.
All you need to do is add CGPoints to some array and use them in the future.
Of course, there are a few difficulties with using CoreGraphics framework, but it is not that tricky.
Live mode
After user added all CGPoints, you now have to convert these points into actual CLLocationCoordinate2D instances.
CGPoint thePoint = ...
CLLocationCoordinate2D theLocationCoordinate2D = [theMapView convertPoint:thePoint toCoordinateFromView:theDrawingView];
What Trax have in their app are probably (almost certainly) instances of MKPolygon class.
You can add it like so:
NSUInteger theCount = self.theMainDrawingView.thePointsArray.count;
CLLocationCoordinate2D theLocationCoordinatesArray[theCount];
for (NSUInteger theIndex = 0; theIndex < theCount; theIndex++)
{
CGPoint thePoint = self.theMainDrawingView.thePointsArray[theIndex].CGPointValue;
CLLocationCoordinate2D theLocationCoordinate2D = [self.theMainMapView convertPoint:thePoint toCoordinateFromView:self.theMainDrawingView];
theLocationCoordinatesArray[theIndex] = theLocationCoordinate2D;
}
MKPolygon *thePolygon = [MKPolygon polygonWithCoordinates:theLocationCoordinatesArray count:theCount];
[self.theMainMapView addOverlay:thePolygon];
However, this is not the end. This will trigger a delegate method of your MKMapView (don't forget to set its .delegate property)
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay
{
if (![overlay isKindOfClass:[MKPolygon class]])
{
return nil;
}
MKPolygonView *thePolygonView = [[MKPolygonView alloc] initWithPolygon:(MKPolygon *)overlay];
thePolygonView.fillColor = [[UIColor cyanColor] colorWithAlphaComponent:0.2];
thePolygonView.strokeColor = [[UIColor redColor] colorWithAlphaComponent:0.6];
thePolygonView.lineWidth = 4;
return thePolygonView;
}
Result looks like this:
Summary
Of course, this doesn't fully solve the issue, because you would have to add pinch and pan gestures too, but I hope that I could point into the right direction.

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)

iOS: CAShapeLayer to draw non-rectagular image and animating its shape

I have been reading the documentation on CAShapeLayer but I still don't quite get it.
From my understanding, Layer is always flat and its size is always a rectangle.
CAShapeLayer on the other hand, allows you to define a layer that is not just rectangle-like. It can be a circle shape, triangle etc as long as you use it with UIBezierPaths.
Is my understanding off here?
What I had in mind is, for example, a tennis ball that bounces off the edges on the screen (easy enough), but I would like to show a little animation not using image animations - I would like it to show a little "squeezed" like animation as it hits the edge of the screen and then bounces off. I am not using a tennis ball image. Just a yellow color filled circle.
Am I correct here with CAShapeLayer to accomplish this? If so, can you please provide a litle example? Thanks.
While you do use a path to define the shape of the layer, it is still created with a rectangle used to define the frame/bounds. Here is an example to get you started:
TennisBall.h:
#import <UIKit/UIKit.h>
#interface TennisBall : UIView
- (void)bounce;
#end
TennisBall.m:
#import <QuartzCore/QuartzCore.h>
#import "TennisBall.h"
#implementation TennisBall
+ (Class)layerClass
{
return [CAShapeLayer class];
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setupLayer];
}
return self;
}
/* Create the tennis ball */
- (void)setupLayer
{
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
layer.strokeColor = [[UIColor blackColor] CGColor];
layer.fillColor = [[UIColor yellowColor] CGColor];
layer.lineWidth = 1.5;
layer.path = [self defaultPath];
}
/* Animate the tennis ball "bouncing" off of the side of the screen */
- (void)bounce
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.duration = 0.2;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = (__bridge id)[self defaultPath];
animation.toValue = (__bridge id)[self compressedPath];
animation.autoreverses = YES;
[self.layer addAnimation:animation forKey:#"animatePath"];
}
/* A path representing the tennis ball in the default state */
- (CGPathRef)defaultPath
{
return [[UIBezierPath bezierPathWithOvalInRect:self.frame] CGPath];
}
/* A path representing the tennis ball is the compressed state (during the bounce) */
- (CGPathRef)compressedPath
{
CGRect newFrame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width * 0.85, self.frame.size.height);
return [[UIBezierPath bezierPathWithOvalInRect:newFrame] CGPath];
}
#end
Now, when you want to use this:
TennisBall *ball = [[TennisBall alloc] initWithFrame:CGRectMake(10, 10, 100, 100)];
[self.view addSubview:ball];
[ball bounce];
Note that this will need to be extended so that you can "bounce" from different angles, etc. but it should get you pointed in the right direction!

How to make my UIBezierPath animated with CAShapeLayer?

I'm trying to animate a UIBezierPath and I've installed a CAShapeLayer to try to do it. Unfortunately the animation isn't working and I'm not sure any of the layers are having any affect (as the code is doing the same thing it was doing before I had the layers).
Here is the actual code - would love any help. Draw2D is an implementation of UIView that is embedded in a UIViewController. All the drawing is happening inside the Draw2D class. The call to [_helper createDrawing... ] simply populates the _uipath variable with points.
Draw2D.h defines the following properties:
#define defaultPointCount ((int) 25)
#property Draw2DHelper *helper;
#property drawingTypes drawingType;
#property int graphPoints;
#property UIBezierPath *uipath;
#property CALayer *animationLayer;
#property CAShapeLayer *pathLayer;
- (void)refreshRect:(CGRect)rect;
below is the actual implementation :
//
// Draw2D.m
// Draw2D
//
// Created by Marina on 2/19/13.
// Copyright (c) 2013 Marina. All rights reserved.
//
#import "Draw2D.h"
#import"Draw2DHelper.h"
#include <stdlib.h>
#import "Foundation/Foundation.h"
#import <QuartzCore/QuartzCore.h>
int MAX_WIDTH;
int MAX_HEIGHT;
#implementation Draw2D
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code
if (self.pathLayer != nil) {
[self.pathLayer removeFromSuperlayer];
self.pathLayer = nil;
}
self.animationLayer = [CALayer layer];
self.animationLayer.frame = self.bounds;
[self.layer addSublayer:self.animationLayer];
CAShapeLayer *l_pathLayer = [CAShapeLayer layer];
l_pathLayer.frame = self.frame;
l_pathLayer.bounds = self.bounds;
l_pathLayer.geometryFlipped = YES;
l_pathLayer.path = _uipath.CGPath;
l_pathLayer.strokeColor = [[UIColor grayColor] CGColor];
l_pathLayer.fillColor = nil;
l_pathLayer.lineWidth = 1.5f;
l_pathLayer.lineJoin = kCALineJoinBevel;
[self.animationLayer addSublayer:l_pathLayer];
self.pathLayer = l_pathLayer;
[self.layer addSublayer:l_pathLayer];
}
return self;
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect :(int) points :(drawingTypes) type //:(Boolean) initial
{
//CGRect bounds = [[UIScreen mainScreen] bounds];
CGRect appframe= [[UIScreen mainScreen] applicationFrame];
CGContextRef context = UIGraphicsGetCurrentContext();
_helper = [[Draw2DHelper alloc ] initWithBounds :appframe.size.width :appframe.size.height :type];
CGPoint startPoint = [_helper generatePoint] ;
[_uipath moveToPoint:startPoint];
[_uipath setLineWidth: 1.5];
CGContextSetStrokeColorWithColor(context, [UIColor lightGrayColor].CGColor);
CGPoint center = CGPointMake(self.center.y, self.center.x) ;
[_helper createDrawing :type :_uipath :( (points>0) ? points : defaultPointCount) :center];
self.pathLayer.path = (__bridge CGPathRef)(_uipath);
[_uipath stroke];
[self startAnimation];
}
- (void) startAnimation {
[self.pathLayer removeAllAnimations];
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 3.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
}
- (void)drawRect:(CGRect)rect {
if (_uipath == NULL)
_uipath = [[UIBezierPath alloc] init];
else
[_uipath removeAllPoints];
[self drawRect:rect :self.graphPoints :self.drawingType ];
}
- (void)refreshRect:(CGRect)rect {
[self setNeedsDisplay];
}
#end
I know there's probably an obvious reason for why the path isn't animating as it's being drawing (as opposed to being shown immediately which is what happens now) but I've been staring at the thing for so long that I just don't see it.
Also, if anyone can recommend a basic primer on CAShapeLayers and animation in general I would appreciate it. Haven't come up across any that are good enough.
thanks in advance.
It looks like you're trying to animate within drawRect (indirectly, at least). That doesn't quite make sense. You don't animate within drawRect. The drawRect is used for drawing a single frame. Some animation is done with timers or CADisplayLink that repeatedly calls setNeedsDisplay (which will cause iOS to call your drawRect) during which you might draw the single frame that shows the progress of the animation at that point. But you simply don't have drawRect initiating any animation on its own.
But, since you're using Core Animation's CAShapeLayer and CABasicAnimation, you don't need a custom drawRect at all. Quartz's Core Animation just takes care of everything for you. For example, here is my code for animating the drawing of a UIBezierPath:
#import <QuartzCore/QuartzCore.h>
#interface View ()
#property (nonatomic, weak) CAShapeLayer *pathLayer;
#end
#implementation View
/*
// I'm not doing anything here, so I can comment this out
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
*/
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
}
*/
// It doesn't matter what my path is. I could make it anything I wanted.
- (UIBezierPath *)samplePath
{
UIBezierPath *path = [UIBezierPath bezierPath];
// build the path here
return path;
}
- (void)startAnimation
{
if (self.pathLayer == nil)
{
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [[self samplePath] CGPath];
shapeLayer.strokeColor = [[UIColor grayColor] CGColor];
shapeLayer.fillColor = nil;
shapeLayer.lineWidth = 1.5f;
shapeLayer.lineJoin = kCALineJoinBevel;
[self.layer addSublayer:shapeLayer];
self.pathLayer = shapeLayer;
}
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 3.0;
pathAnimation.fromValue = #(0.0f);
pathAnimation.toValue = #(1.0f);
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
}
#end
Then, when I want to start drawing the animation, I just call my startAnimation method. I probably don't even need a UIView subclass at all for something as simple as this, since I'm not actually changing any UIView behavior. There are definitely times that you subclass UIView with a custom drawRect implementation, but it's not needed here.
You asked for some references:
I would probably start with a review of Apple's Core Animation Programming Guide, if you haven't seen that.
For me, it all fell into place when I went through Mike Nachbaur's Core Animation Tutorial Part 4, actually reproducing his demo from scratch. Clearly, you can check out parts 1 through 3, too.

Resources