IOS: Touch outside layer mask but inside frame - ios

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;
}

Related

Merge background color and placeholder image in one image

I am building a scratch card experience where I have a background color and a transparent png on the top.
Beneath this, I have the actual image with the content of the scratch card.
I want to combine the background color and the transparent image as one uiimage so that when i scratch this, I am able to see the below content.
I have tried putting a background color to the actual image but when I scratch it, I cannot see the content. Instead the background color starts clearing the transparent image.
I have written the following code for clearing the area of the image after i touch it:
- (UIImage *)addTouches:(NSSet *)touches {
CGSize size = CGSizeMake(self.image.size.width * self.image.scale, self.image.size.height * self.image.scale);
CGContextRef ctx = _imageContext;
CGContextSetFillColorWithColor(ctx,[UIColor clearColor].CGColor);
CGContextSetStrokeColorWithColor(ctx,[UIColor colorWithRed:0 green:0 blue:0 alpha:0].CGColor);
int tempFilled = _tilesFilled;
// process touches
for (UITouch *touch in touches) {
CGContextBeginPath(ctx);
CGPoint touchPoint = [touch locationInView:self];
touchPoint = fromUItoQuartz(touchPoint, self.bounds.size);
touchPoint = scalePoint(touchPoint, self.bounds.size, size);
if(UITouchPhaseBegan == touch.phase){
[self.touchPoints removeAllObjects];
[self.touchPoints addObject:[NSValue valueWithCGPoint:touchPoint]];
[self.touchPoints addObject:[NSValue valueWithCGPoint:touchPoint]];
// on begin, we just draw ellipse
CGRect rect = CGRectMake(touchPoint.x - _radius, touchPoint.y - _radius, _radius*2, _radius*2);
CGContextAddEllipseInRect(ctx, rect);
CGContextFillPath(ctx);
static const FillTileWithPointFunc fillTileFunc = (FillTileWithPointFunc) [self methodForSelector:#selector(fillTileWithPoint:)];
(*fillTileFunc)(self,#selector(fillTileWithPoint:),rect.origin);
} else if (UITouchPhaseMoved == touch.phase) {
[self.touchPoints addObject:[NSValue valueWithCGPoint:touchPoint]];
// then touch moved, we draw superior-width line
CGContextSetStrokeColor(ctx, CGColorGetComponents([UIColor clearColor].CGColor));
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetLineWidth(ctx, 2 * _radius);
// CGContextMoveToPoint(ctx, prevPoint.x, prevPoint.y);
// CGContextAddLineToPoint(ctx, rect.origin.x, rect.origin.y);
while(self.touchPoints.count > 3){
CGPoint bezier[4];
bezier[0] = ((NSValue*)self.touchPoints[1]).CGPointValue;
bezier[3] = ((NSValue*)self.touchPoints[2]).CGPointValue;
CGFloat k = 0.3;
CGFloat len = sqrt(pow(bezier[3].x - bezier[0].x, 2) + pow(bezier[3].y - bezier[0].y, 2));
bezier[1] = ((NSValue*)self.touchPoints[0]).CGPointValue;
bezier[1] = [self normalizeVector:CGPointMake(bezier[0].x - bezier[1].x - (bezier[0].x - bezier[3].x), bezier[0].y - bezier[1].y - (bezier[0].y - bezier[3].y) )];
bezier[1].x *= len * k;
bezier[1].y *= len * k;
bezier[1].x += bezier[0].x;
bezier[1].y += bezier[0].y;
bezier[2] = ((NSValue*)self.touchPoints[3]).CGPointValue;
bezier[2] = [self normalizeVector:CGPointMake( (bezier[3].x - bezier[2].x) - (bezier[3].x - bezier[0].x), (bezier[3].y - bezier[2].y) - (bezier[3].y - bezier[0].y) )];
bezier[2].x *= len * k;
bezier[2].y *= len * k;
bezier[2].x += bezier[3].x;
bezier[2].y += bezier[3].y;
CGContextMoveToPoint(ctx, bezier[0].x, bezier[0].y);
CGContextAddCurveToPoint(ctx, bezier[1].x, bezier[1].y, bezier[2].x, bezier[2].y, bezier[3].x, bezier[3].y);
[self.touchPoints removeObjectAtIndex:0];
}
CGContextStrokePath(ctx);
CGPoint prevPoint = [touch previousLocationInView:self];
prevPoint = fromUItoQuartz(prevPoint, self.bounds.size);
prevPoint = scalePoint(prevPoint, self.bounds.size, size);
static const FillTileWithTwoPointsFunc fillTileFunc = (FillTileWithTwoPointsFunc) [self methodForSelector:#selector(fillTileWithTwoPoints:end:)];
(*fillTileFunc)(self,#selector(fillTileWithTwoPoints:end:),touchPoint, prevPoint);
}
}
// was _tilesFilled changed?
if(tempFilled != _tilesFilled) {
[_delegate mdScratchImageView:self didChangeMaskingProgress:self.maskingProgress];
}
CGImageRef cgImage = CGBitmapContextCreateImage(ctx);
UIImage *image = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
return image;
}
/*
* filling tile with one ellipse
*/
-(void)fillTileWithPoint:(CGPoint) point{
size_t x,y;
point.x = MAX( MIN(point.x, self.image.size.width - 1) , 0);
point.y = MAX( MIN(point.y, self.image.size.height - 1), 0);
x = point.x * self.maskedMatrix.max.x / self.image.size.width;
y = point.y * self.maskedMatrix.max.y / self.image.size.height;
char value = [self.maskedMatrix valueForCoordinates:x y:y];
if (!value){
[self.maskedMatrix setValue:1 forCoordinates:x y:y];
_tilesFilled++;
}
}
/*
* filling tile with line
*/
-(void)fillTileWithTwoPoints:(CGPoint)begin end:(CGPoint)end{
CGFloat incrementerForx,incrementerFory;
static const FillTileWithPointFunc fillTileFunc = (FillTileWithPointFunc) [self methodForSelector:#selector(fillTileWithPoint:)];
/* incrementers - about size of a tile */
incrementerForx = (begin.x < end.x ? 1 : -1) * self.image.size.width / _tilesX;
incrementerFory = (begin.y < end.y ? 1 : -1) * self.image.size.height / _tilesY;
// iterate on points between begin and end
CGPoint i = begin;
while(i.x <= MAX(begin.x, end.x) && i.y <= MAX(begin.y, end.y) && i.x >= MIN(begin.x, end.x) && i.y >= MIN(begin.y, end.y)){
(*fillTileFunc)(self,#selector(fillTileWithPoint:),i);
i.x += incrementerForx;
i.y += incrementerFory;
}
(*fillTileFunc)(self,#selector(fillTileWithPoint:),end);
}
What you probably want to do is use a Layer Mask.
When masking a layer, from Apple's docs:
The layer’s alpha channel determines how much of the layer’s content and background shows through. Fully or partially opaque pixels allow the underlying content to show through, but fully transparent pixels block that content.
So, you'd want to use a path to mask your dark circle.
However, to get the "scratch off" effect, you would need to draw the path with a Clear stroke... which you cannot accomplish with a CAShapeLayer.
So, we'll use a custom CALayer subclass.
MyShapeLayer.h
//
// MyShapeLayer.h
//
#import <QuartzCore/QuartzCore.h>
#interface MyShapeLayer : CALayer
#property(nonatomic) CGPathRef path;
#end
MyShapeLayer.m
//
// MyShapeLayer.m
//
#import <UIKit/UIKit.h>
#import "MyShapeLayer.h"
#implementation MyShapeLayer
- (void)drawInContext:(CGContextRef)inContext {
// fill entire layer with solid color
CGContextSetGrayFillColor(inContext, 0.0, 1.0);
CGContextFillRect(inContext, self.bounds);
// we want to "clear" the stroke
CGContextSetStrokeColorWithColor(inContext, [UIColor clearColor].CGColor);
// any color will work, as the mask uses the alpha value
CGContextSetFillColorWithColor(inContext, [UIColor whiteColor].CGColor);
// adjust drawing-line-width as desired
CGContextSetLineWidth(inContext, 60.0);
CGContextSetLineCap(inContext, kCGLineCapRound);
CGContextSetLineJoin(inContext, kCGLineJoinRound);
CGContextAddPath(inContext, self.path);
CGContextSetBlendMode(inContext, kCGBlendModeSourceIn);
CGContextDrawPath(inContext, kCGPathFillStroke);
}
#end
Now we can create a UIView subclass to draw a filled-circle path on a CAShapeLayer and mask it with our MyShapeLayer.
ScratchOffView.h
//
// ScratchOffView.h
//
#import <UIKit/UIKit.h>
#interface ScratchOffView : UIView
#property (assign, readwrite) CGFloat expandedBounds;
#end
ScratchOffView.m
//
// ScratchOffView.m
//
#import "ScratchOffView.h"
#import "MyShapeLayer.h"
#interface ScratchOffView()
#property (strong, nonatomic) UIBezierPath *maskPath;
#property (strong, nonatomic) MyShapeLayer *maskLayer;
#property (strong, nonatomic) CAShapeLayer *scratchOffShapeLayer;
#property (strong, nonatomic) CALayer *scratchOffLayer;
#end
#implementation ScratchOffView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (void)commonInit {
_maskPath = [UIBezierPath new];
_maskLayer = [MyShapeLayer new];
_scratchOffLayer = [CALayer new];
_scratchOffShapeLayer = [CAShapeLayer new];
// Important, otherwise you will get a black rectangle
_maskLayer.opaque = NO;
// add the layer holding the shape to "Scratch Off"
[self.layer addSublayer:_scratchOffShapeLayer];
UIColor *c = [UIColor colorWithRed:50.0 / 255.0 green:150.0 / 255.0 blue:140.0 / 255.0 alpha:1.0];
[_scratchOffShapeLayer setFillColor:c.CGColor];
// set the mask layer
[_scratchOffShapeLayer setMask:_maskLayer];
// default 0.0 == no expanded bounds for touch
_expandedBounds = 0.0;
}
- (void)layoutSubviews {
[super layoutSubviews];
[_maskLayer setFrame:[self bounds]];
[_scratchOffShapeLayer setFrame:[self bounds]];
UIBezierPath *b = [UIBezierPath bezierPathWithOvalInRect:[self bounds]];
[_scratchOffShapeLayer setPath:b.CGPath];
// triggers drawInContext
[_maskLayer setNeedsDisplay];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self];
[_maskPath moveToPoint:currentPoint];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self];
// add line to our maskPath
[_maskPath addLineToPoint:currentPoint];
// update the mask layer path
[_maskLayer setPath:_maskPath.CGPath];
// triggers drawInContext
[_maskLayer setNeedsDisplay];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// accept touch if within expanded bounds
// setting _expandedBounds to a Positive number allows the
// touches to start outside the frame
CGRect r = CGRectInset([self bounds], -_expandedBounds, -_expandedBounds);
return CGRectContainsPoint(r, point);
}
#end
Note that we've added a property: expandedBounds. Since the touches will only register if they begin on this view, we can (virtually) expand the bounds of the view so the user can touch and "drag into the circle."
Here is a complete example implementation. To try and match your question, I use this image (420 x 460 pixels) as the "background" image:
and this image (284 x 284 pixels) as the "image to reveal under the scratch-off circle" (the transparent area is the size we want the circle to be):
ScratchOffTestViewController.h
//
// ScratchOffTestViewController.h
//
#import <UIKit/UIKit.h>
#interface ScratchOffTestViewController : UIViewController
#end
ScratchOffTestViewController.m
//
// ScratchOffTestViewController.m
//
#import "ScratchOffTestViewController.h"
#import "ScratchOffView.h"
#interface ScratchOffTestViewController ()
#property (strong, nonatomic) ScratchOffView *scratchOffView;
#end
#implementation ScratchOffTestViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// create the Scratch Off View
_scratchOffView = [ScratchOffView new];
// load background and giftBox image
UIImage *bkgImage = [UIImage imageNamed:#"backgroundImage"];
UIImage *giftBoxImage = [UIImage imageNamed:#"giftBox"];
if (!bkgImage || !giftBoxImage) {
NSLog(#"Could not load images!!!");
return;
}
UIImageView *bkgImageView = [UIImageView new];
UIImageView *giftImageView = [UIImageView new];
bkgImageView.image = bkgImage;
giftImageView.image = giftBoxImage;
bkgImageView.translatesAutoresizingMaskIntoConstraints = NO;
giftImageView.translatesAutoresizingMaskIntoConstraints = NO;
_scratchOffView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:bkgImageView];
[self.view addSubview:giftImageView];
[self.view addSubview:_scratchOffView];
UILayoutGuide *g = [self.view safeAreaLayoutGuide];
[NSLayoutConstraint activateConstraints:#[
// constrain background image view to background image size
[bkgImageView.widthAnchor constraintEqualToConstant:bkgImage.size.width],
[bkgImageView.heightAnchor constraintEqualToConstant:bkgImage.size.height],
// centered
[bkgImageView.centerXAnchor constraintEqualToAnchor:g.centerXAnchor],
[bkgImageView.centerYAnchor constraintEqualToAnchor:g.centerYAnchor],
// constrain giftBox image view to giftBox image size
[giftImageView.widthAnchor constraintEqualToConstant:giftBoxImage.size.width],
[giftImageView.heightAnchor constraintEqualToConstant:giftBoxImage.size.height],
// centered horizontally, and a little above vertically
[giftImageView.centerXAnchor constraintEqualToAnchor:bkgImageView.centerXAnchor],
[giftImageView.centerYAnchor constraintEqualToAnchor:bkgImageView.centerYAnchor],
// constrain Scratch Off View to giftImageView
[_scratchOffView.widthAnchor constraintEqualToAnchor:giftImageView.widthAnchor],
[_scratchOffView.heightAnchor constraintEqualToAnchor:giftImageView.widthAnchor],
[_scratchOffView.centerXAnchor constraintEqualToAnchor:giftImageView.centerXAnchor],
[_scratchOffView.centerYAnchor constraintEqualToAnchor:giftImageView.centerYAnchor],
]];
// expand the touch bounds of the Scratch Off View by 80-pts
_scratchOffView.expandedBounds = 80.0;
return;
}
#end
On start, we see this:
and after touch-drag a bit on the circle, we see this:
If we continue dragging our touch around, the dark-green circle will eventually be completely gone -- we will have "scratched it off."

Core Graphics angle gradient for gauge

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

How to draw a circle according to the offset when drop down the tableView

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];
}

Replacing UIBezierPath with new scaled path

I have a UITableViewCell that contains a UIBezierPath that stretches across the cell. When my tableview goes into editing mode, a portion of the right end of the path goes off the screen when the cell is shifted over. My solution is to scale the path down when I go into edit mode, then scale it back up to its original size when coming out of edit mode. I tried this out and the new scaled down path appears, but the original wider path is still visible. Basically, the new scaled down path is drawn over the original wider path. How can I toggle between two paths to make one disappear and the other one visible.
- (void)didTransitionToState:(UITableViewCellStateMask)state
{
[super didTransitionToState:state];
if (state == UITableViewCellStateShowingEditControlMask)
{
CGRect newFrame = CGRectMake(self.waveView.frame.origin.x, self.waveView.frame.origin.y, self.waveView.frame.size.width -100, self.waveView.frame.size.height);
//Using +100/-100 for testing
CGFloat scaleFactor = (self.waveView.frame.size.width-100)/self.waveView.frame.size.width;
self.waveView.frame = newFrame;
CGAffineTransform transform = CGAffineTransformScale(self.waveView.transform,
scaleFactor,
1);
[self.waveView.path applyTransform:transform];
}
else if (state ==UITableViewCellStateDefaultMask)
{
// normal mode : back to normal
CGRect originalFrame = CGRectMake(self.waveView.frame.origin.x, self.waveView.frame.origin.y, self.waveView.frame.size.width +100, self.waveView.frame.size.height);
CGFloat scaleFactor = (self.waveView.frame.size.width + 100)/self.waveView.frame.size.width;
self.waveView.frame = originalFrame;
CGAffineTransform transform = CGAffineTransformScale(self.waveView.transform,
scaleFactor,
1);
[self.waveView.path applyTransform:transform];
}
}
waveView is a subclass of UIView with the following:
#property (nonatomic, strong) CAShapeLayer* blueWave;
-(void) layoutSubviews
{
[super layoutSubviews];
self.blueWave = [self createShapeLayer];
[self.layer addSublayer:self.blueWave];
[self render];
}
- (void)render
{
self.blueWave.path = self.path.CGPath;
}
- (CAShapeLayer *) createShapeLayer
{
CAShapeLayer* layer = [[CAShapeLayer alloc] init];
layer.lineWidth = 2;
layer.fillColor = nil;
return layer;
}
In cellForRowAtIndex I do the following
cell.waveView.path = [NSKeyedUnarchiver unarchiveObjectWithData:pathData];

Cut Out Shape with Animation

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).

Resources