(Using iOS 5 and Xcode 4.2.)
I've followed the instructions here: http://developer.apple.com/library/ios/#documentation/UserExperience/Conceptual/LocationAwarenessPG/AnnotatingMaps/AnnotatingMaps.html#//apple_ref/doc/uid/TP40009497-CH6-SW15 and used the MKCircle and MKCircleView classes to add a circle overlay on my MKMapView.
However what I actually want is an inverted circle overlay, like the left map in the sketch below (currently I have a circle overlay like the one on the right):
For the inverted circle, the overlay should cover the entire map - apart from the visible circle.
Is there an easy way to accomplish this using the MKCircle/MKCircleView classes? Or will I have to go deeper and define a custom overlay object/view?
Thank you for your help :)
The best way to do it, would be to subclass MKMapView
and override the drawRect
method call super, then paint over the map with the color you want.
Then each time the user moves, drawRect
should respond by drawing appropriately.
I had the same task and here is how I solve it:
NOTE: this code will only work starting from iOS7
Add an overlay to the map, somewhere in your view controller:
MyMapOverlay *overlay = [[MyMapOverlay alloc] initWithCoordinate:coordinate];
[self.mapView addOverlay:overlay level:MKOverlayLevelAboveLabels];
In the MKMapViewDelegate methods write next:
- (MKOverlayRenderer *)mapView:(MKMapView *)map rendererForOverlay:(id<MKOverlay>)overlay {
/// we need to draw overlay on the map in a way when everything except the area in radius of 500 should be grayed
/// to do that there is special renderer implemented - NearbyMapOverlay
if ([overlay isKindOfClass:[NearbyMapOverlay class]]) {
MyMapOverlayRenderer *renderer = [[MyMapOverlayRenderer alloc] initWithOverlay:overlay];
renderer.fillColor = [UIColor whateverColor];/// specify color which you want to use for gray out everything out of radius
renderer.diameterInMeters = 1000;/// choose whatever diameter you need
return renderer;
}
return nil;
}
The MyMapOverlay itself should be something like followed:
@interface MyMapOverlay : NSObject<MKOverlay>
- (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate;
@end
@implementation MyMapOverlay
@synthesize coordinate = _coordinate;
- (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate {
self = [super init];
if (self) {
_coordinate = coordinate;
}
return self;
}
- (MKMapRect)boundingMapRect {
return MKMapRectWorld;
}
@end
And the MyMapOverlayRenderer:
@interface MyMapOverlayRenderer : MKOverlayRenderer
@property (nonatomic, assign) double diameterInMeters;
@property (nonatomic, copy) UIColor *fillColor;
@end
@implementation MyMapOverlayRenderer
/// this method is called as a part of rendering the map, and it draws the overlay polygon by polygon
/// which means that it renders overlay by square pieces
- (void)drawMapRect:(MKMapRect)mapRect
zoomScale:(MKZoomScale)zoomScale
inContext:(CGContextRef)context {
/// main path - whole area
UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(mapRect.origin.x, mapRect.origin.y, mapRect.size.width, mapRect.size.height)];
/// converting to the 'world' coordinates
double radiusInMapPoints = self.diameterInMeters * MKMapPointsPerMeterAtLatitude(self.overlay.coordinate.latitude);
MKMapSize radiusSquared = {radiusInMapPoints, radiusInMapPoints};
MKMapPoint regionOrigin = MKMapPointForCoordinate(self.overlay.coordinate);
MKMapRect regionRect = (MKMapRect){regionOrigin, radiusSquared}; //origin is the top-left corner
regionRect = MKMapRectOffset(regionRect, -radiusInMapPoints/2, -radiusInMapPoints/2);
// clamp the rect to be within the world
regionRect = MKMapRectIntersection(regionRect, MKMapRectWorld);
/// next path is used for excluding the area within the specific radius from current user location, so it will not be filled by overlay fill color
UIBezierPath *excludePath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(regionRect.origin.x, regionRect.origin.y, regionRect.size.width, regionRect.size.height) cornerRadius:regionRect.size.width / 2];
[path appendPath:excludePath];
/// setting overlay fill color
CGContextSetFillColorWithColor(context, self.fillColor.CGColor);
/// adding main path. NOTE that exclusionPath was appended to main path, so we should only add 'path'
CGContextAddPath(context, path.CGPath);
/// tells the context to fill the path but with regards to even odd rule
CGContextEOFillPath(context);
}
As a result you will have exact same view like on the left image that was posted in the question.
Here a Swift version. Thanks Valerii.
https://github.com/dariopellegrini/MKInvertedCircle
i tried to use this swift version and it didn't work, so im posting my implementation (tested on iOS 12)
import Foundation
import UIKit
import MapKit
class MKInvertedCircleOverlayRenderer: MKOverlayRenderer {
var fillColor: UIColor = UIColor.red
var strokeColor: UIColor = UIColor.blue
var lineWidth: CGFloat = 3
var circle: MKCircle
init(circle: MKCircle) {
self.circle = circle
super.init(overlay: circle)
}
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
let path = UIBezierPath(rect: rect(for: MKMapRectWorld))
let excludePath: UIBezierPath = UIBezierPath(roundedRect: CGRect(x: circle.coordinate.latitude,
y: circle.coordinate.longitude,
width: circle.boundingMapRect.size.width,
height: circle.boundingMapRect.size.height),
cornerRadius: CGFloat(circle.boundingMapRect.size.width))
context.setFillColor(fillColor.cgColor)
path.append(excludePath)
context.addPath(path.cgPath)
context.fillPath(using: .evenOdd)
context.addPath(excludePath.cgPath)
context.setLineWidth(9 / zoomScale)
context.setStrokeColor(strokeColor.cgColor)
context.strokePath()
//line showing circle radius
let lineBeginPoint = CGPoint(x: excludePath.bounds.midX, y: excludePath.bounds.midY)
let lineEndPoint = CGPoint(x: excludePath.bounds.maxX, y: excludePath.bounds.midY)
let linePath: UIBezierPath = UIBezierPath()
linePath.move(to: lineBeginPoint)
linePath.addLine(to: lineEndPoint)
context.addPath(linePath.cgPath)
context.setLineWidth(6/zoomScale)
context.setStrokeColor(UIColor.black.cgColor)
context.setLineDash(phase: 1, lengths: [20 / zoomScale, 10 / zoomScale])
context.strokePath()
// circle at the end of the line above
let circleSize: CGFloat = 30/zoomScale
let circleRect = CGRect(origin: CGPoint(x: lineEndPoint.x - (circleSize/2), y: lineEndPoint.y - (circleSize/2)),
size: CGSize(width: circleSize, height: circleSize))
let circlePath: UIBezierPath =
UIBezierPath(roundedRect: circleRect, cornerRadius: circleSize)
context.addPath(circlePath.cgPath)
context.setFillColor(UIColor.black.cgColor)
context.fillPath()
}