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.