Sharp Mask
Whenever you want to draw a path that consists of a shape (or series of shapes) as a hole in another shape, the key is almost always using an 'even odd winding rule'.
From the Winding Rules section of the Cocoa Drawing Guide:
A winding rule is simply an algorithm that tracks information about each contiguous region that makes up the path's overall fill area. A ray is drawn from a point inside a given region to any point outside the path bounds. The total number of crossed path lines (including implicit lines) and the direction of each path line are then interpreted using rules which determine if the region should be filled.
I appreciate that description isn't really helpful without the rules as context and diagrams to make it easier to understand so I urge you to read the links I've provided above. For the sake of creating our circle mask layer the following diagrams depict what an even odd winding rule allows us to accomplish:
Non Zero Winding Rule
Even Odd Winding Rule
Now it's simply a matter of creating the translucent mask using a CAShapeLayer that can be repositioned and expanded and contracted through user interaction.
Code
#import <QuartzCore/QuartzCore.h>
@interface ViewController ()
@property (strong, nonatomic) IBOutlet UIImageView *imageView;
@property (strong) CAShapeLayer *blurFilterMask;
@property (assign) CGPoint blurFilterOrigin;
@property (assign) CGFloat blurFilterDiameter;
@end
@implementation ViewController
// begin the blur masking operation.
- (void)beginBlurMasking
{
self.blurFilterOrigin = self.imageView.center;
self.blurFilterDiameter = MIN(CGRectGetWidth(self.imageView.bounds), CGRectGetHeight(self.imageView.bounds));
CAShapeLayer *blurFilterMask = [CAShapeLayer layer];
// Disable implicit animations for the blur filter mask's path property.
blurFilterMask.actions = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNull null], @"path", nil];
blurFilterMask.fillColor = [UIColor blackColor].CGColor;
blurFilterMask.fillRule = kCAFillRuleEvenOdd;
blurFilterMask.frame = self.imageView.bounds;
blurFilterMask.opacity = 0.5f;
self.blurFilterMask = blurFilterMask;
[self refreshBlurMask];
[self.imageView.layer addSublayer:blurFilterMask];
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
[self.imageView addGestureRecognizer:tapGesture];
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
[self.imageView addGestureRecognizer:pinchGesture];
}
// Move the origin of the blur mask to the location of the tap.
- (void)handleTap:(UITapGestureRecognizer *)sender
{
self.blurFilterOrigin = [sender locationInView:self.imageView];
[self refreshBlurMask];
}
// Expand and contract the clear region of the blur mask.
- (void)handlePinch:(UIPinchGestureRecognizer *)sender
{
// Use some combination of sender.scale and sender.velocity to determine the rate at which you want the circle to expand/contract.
self.blurFilterDiameter += sender.velocity;
[self refreshBlurMask];
}
// Update the blur mask within the UI.
- (void)refreshBlurMask
{
CGFloat blurFilterRadius = self.blurFilterDiameter * 0.5f;
CGMutablePathRef blurRegionPath = CGPathCreateMutable();
CGPathAddRect(blurRegionPath, NULL, self.imageView.bounds);
CGPathAddEllipseInRect(blurRegionPath, NULL, CGRectMake(self.blurFilterOrigin.x - blurFilterRadius, self.blurFilterOrigin.y - blurFilterRadius, self.blurFilterDiameter, self.blurFilterDiameter));
self.blurFilterMask.path = blurRegionPath;
CGPathRelease(blurRegionPath);
}
...
(This diagram may help understand the naming conventions in the code)
Gradient Mask
The Gradients section of Apple's Quartz 2D Programming Guide details how to draw radial gradients which we can use to create a mask with a feathered edge. This involves drawing a CALayers content directly by subclassing it or implementing its drawing delegate. Here we subclass it to encapsulate the data related to it i.e. origin and diameter.
Code
BlurFilterMask.h
#import <QuartzCore/QuartzCore.h>
@interface BlurFilterMask : CALayer
@property (assign) CGPoint origin; // The centre of the blur filter mask.
@property (assign) CGFloat diameter; // the diameter of the clear region of the blur filter mask.
@end
BlurFilterMask.m
#import "BlurFilterMask.h"
// The width in points the gradated region of the blur filter mask will span over.
CGFloat const GRADIENT_WIDTH = 50.0f;
@implementation BlurFilterMask
- (void)drawInContext:(CGContextRef)context
{
CGFloat clearRegionRadius = self.diameter * 0.5f;
CGFloat blurRegionRadius = clearRegionRadius + GRADIENT_WIDTH;
CGColorSpaceRef baseColorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat colours[8] = { 0.0f, 0.0f, 0.0f, 0.0f, // Clear region colour.
0.0f, 0.0f, 0.0f, 0.5f }; // Blur region colour.
CGFloat colourLocations[2] = { 0.0f, 0.4f };
CGGradientRef gradient = CGGradientCreateWithColorComponents (baseColorSpace, colours, colourLocations, 2);
CGContextDrawRadialGradient(context, gradient, self.origin, clearRegionRadius, self.origin, blurRegionRadius, kCGGradientDrawsAfterEndLocation);
CGColorSpaceRelease(baseColorSpace);
CGGradientRelease(gradient);
}
@end
ViewController.m (Wherever you are implementing the blur filer masking functionality)
#import "ViewController.h"
#import "BlurFilterMask.h"
#import <QuartzCore/QuartzCore.h>
@interface ViewController ()
@property (strong, nonatomic) IBOutlet UIImageView *imageView;
@property (strong) BlurFilterMask *blurFilterMask;
@end
@implementation ViewController
// Begin the blur filter masking operation.
- (void)beginBlurMasking
{
BlurFilterMask *blurFilterMask = [BlurFilterMask layer];
blurFilterMask.diameter = MIN(CGRectGetWidth(self.imageView.bounds), CGRectGetHeight(self.imageView.bounds));
blurFilterMask.frame = self.imageView.bounds;
blurFilterMask.origin = self.imageView.center;
blurFilterMask.shouldRasterize = YES;
[self.imageView.layer addSublayer:blurFilterMask];
[blurFilterMask setNeedsDisplay];
self.blurFilterMask = blurFilterMask;
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
[self.imageView addGestureRecognizer:tapGesture];
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
[self.imageView addGestureRecognizer:pinchGesture];
}
// Move the origin of the blur mask to the location of the tap.
- (void)handleTap:(UITapGestureRecognizer *)sender
{
self.blurFilterMask.origin = [sender locationInView:self.imageView];
[self.blurFilterMask setNeedsDisplay];
}
// Expand and contract the clear region of the blur mask.
- (void)handlePinch:(UIPinchGestureRecognizer *)sender
{
// Use some combination of sender.scale and sender.velocity to determine the rate at which you want the mask to expand/contract.
self.blurFilterMask.diameter += sender.velocity;
[self.blurFilterMask setNeedsDisplay];
}
...
(This diagram may help understand the naming conventions in the code)
Note
Ensure the multipleTouchEnabled
property of the UIImageView
hosting your image is set to YES
/true
:
Note
For sake of clarity in answering the OPs question this answer continues to use the naming conventions originally used. This may be slightly misleading to others. 'Mask' is this context does not refer to an image mask but mask in a more general sense. This answer doesn't use any image masking operations.