There seems to be an \"issue\" with overlays and the MapKit
. Unlike annotations, overlays aren\'t reused and therefore when adding multiple overlays would cause
The Answer to this is not "reusing" but to draw them all in to one MKOverlayView
and then draw that on the map.
Multiple MKPolygons
, MKOverlays
etc. cause heavy memory-usage when drawing on the map. This is due the NOT reusing of overlays by MapKit
. As annotations have the reuseWithIdentifier
, overlays however don't. Each overlay creates a new layer as a MKOverlayView
on the map with the overlay in it. In that way memory-usage will rise quite fast and map-usage becomes... let's say sluggish to almost impossible.
Therefore there is a work-around: Instead of plotting each overlay individually, you can add all of the MKOverlays
to one MKOverlayView
. This way you're in fact creating only one MKOverlayView
and thus there's no need to reuse.
This is the work-around, in this case for MKPolygons
but should be not very different for others like MKCircles
etc.
create a class: MultiPolygon
(subclass of NSObject
)
in MultiPolygon.h
:
#import <MapKit/MapKit.h> //Add import MapKit
@interface MultiPolygon : NSObject <MKOverlay> {
NSArray *_polygons;
MKMapRect _boundingMapRect;
}
- (id)initWithPolygons:(NSArray *)polygons;
@property (nonatomic, readonly) NSArray *polygons;
@end
in MultiPolygon.m
:
@implementation MultiPolygon
@synthesize polygons = _polygons;
- (id)initWithPolygons:(NSArray *)polygons
{
if (self = [super init]) {
_polygons = [polygons copy];
NSUInteger polyCount = [_polygons count];
if (polyCount) {
_boundingMapRect = [[_polygons objectAtIndex:0] boundingMapRect];
NSUInteger i;
for (i = 1; i < polyCount; i++) {
_boundingMapRect = MKMapRectUnion(_boundingMapRect, [[_polygons objectAtIndex:i] boundingMapRect]);
}
}
}
return self;
}
- (MKMapRect)boundingMapRect
{
return _boundingMapRect;
}
- (CLLocationCoordinate2D)coordinate
{
return MKCoordinateForMapPoint(MKMapPointMake(MKMapRectGetMidX(_boundingMapRect), MKMapRectGetMidY(_boundingMapRect)));
}
@end
Now create a class: MultiPolygonView
(subclass of MKOverlayPathView
)
in MultiPolygonView.h
:
#import <MapKit/MapKit.h>
@interface MultiPolygonView : MKOverlayPathView
@end
In MultiPolygonView.m
:
#import "MultiPolygon.h" //Add import "MultiPolygon.h"
@implementation MultiPolygonView
- (CGPathRef)polyPath:(MKPolygon *)polygon
{
MKMapPoint *points = [polygon points];
NSUInteger pointCount = [polygon pointCount];
NSUInteger i;
if (pointCount < 3)
return NULL;
CGMutablePathRef path = CGPathCreateMutable();
for (MKPolygon *interiorPolygon in polygon.interiorPolygons) {
CGPathRef interiorPath = [self polyPath:interiorPolygon];
CGPathAddPath(path, NULL, interiorPath);
CGPathRelease(interiorPath);
}
CGPoint relativePoint = [self pointForMapPoint:points[0]];
CGPathMoveToPoint(path, NULL, relativePoint.x, relativePoint.y);
for (i = 1; i < pointCount; i++) {
relativePoint = [self pointForMapPoint:points[i]];
CGPathAddLineToPoint(path, NULL, relativePoint.x, relativePoint.y);
}
return path;
}
- (void)drawMapRect:(MKMapRect)mapRect
zoomScale:(MKZoomScale)zoomScale
inContext:(CGContextRef)context
{
MultiPolygon *multiPolygon = (MultiPolygon *)self.overlay;
for (MKPolygon *polygon in multiPolygon.polygons) {
CGPathRef path = [self polyPath:polygon];
if (path) {
[self applyFillPropertiesToContext:context atZoomScale:zoomScale];
CGContextBeginPath(context);
CGContextAddPath(context, path);
CGContextDrawPath(context, kCGPathEOFill);
[self applyStrokePropertiesToContext:context atZoomScale:zoomScale];
CGContextBeginPath(context);
CGContextAddPath(context, path);
CGContextStrokePath(context);
CGPathRelease(path);
}
}
}
@end
To us it import MultiPolygon.h
and MultiPolygonView.h
in your ViewController
Create one polygon from all:
As an example I've got an array with polygons: polygonsInArray
.
MultiPolygon *allPolygonsInOne = [[MultiPolygon alloc] initWithPolygons:polygonsInArray];
Add the allPolygonsInOne to the mapView:
[mapView addOverlay:allPolygonsInOne];
Also change your viewForOverlay
method:
-(MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay
{
if ([overlay isKindOfClass:[MultiPolygon class]]) {
MultiPolygonView *polygonsView = [[MultiPolygonView alloc] initWithOverlay:(MultiPolygon*)overlay];
polygonsView.fillColor = [[UIColor magentaColor] colorWithAlphaComponent:0.8];
polygonsView.strokeColor = [[UIColor blueColor] colorWithAlphaComponent:0.8];
polygonsView.lineWidth = 1;
return polygonsView;
}
else {
return nil;
}
}
And this greatly reduced memory usage for multiple overlays on the mapView
. You're not reusing now because only one OverlayView
is drawn. So no need to reuse.
A Swift 4 version of the Objective-C code posted by @wkberg:
MultiPolygon.swift:
import MapKit
/// A concatenation of multiple polygons to allow a single overlay to be drawn in the map,
/// which will consume less resources
class MultiPolygon: NSObject, MKOverlay {
var polygons: [MKPolygon]?
var boundingMapRect: MKMapRect
init(polygons: [MKPolygon]?) {
self.polygons = polygons
self.boundingMapRect = MKMapRect.null
super.init()
guard let pols = polygons else { return }
for (index, polygon) in pols.enumerated() {
if index == 0 { self.boundingMapRect = polygon.boundingMapRect; continue }
boundingMapRect = boundingMapRect.union(polygon.boundingMapRect)
}
}
var coordinate: CLLocationCoordinate2D {
return MKMapPoint(x: boundingMapRect.midX, y: boundingMapRect.maxY).coordinate
}
}
MultiPolygonPathRenderer.swift:
import MapKit
/// A MKOverlayPathRenderer that can draw a concatenation of multiple polygons as a single polygon
/// This will consume less resources
class MultiPolygonPathRenderer: MKOverlayPathRenderer {
/**
Returns a `CGPath` equivalent to this polygon in given renderer.
- parameter polygon: MKPolygon defining coordinates that will be drawn.
- returns: Path equivalent to this polygon in given renderer.
*/
func polyPath(for polygon: MKPolygon?) -> CGPath? {
guard let polygon = polygon else { return nil }
let points = polygon.points()
if polygon.pointCount < 3 { return nil }
let pointCount = polygon.pointCount
let path = CGMutablePath()
if let interiorPolygons = polygon.interiorPolygons {
for interiorPolygon in interiorPolygons {
guard let interiorPath = polyPath(for: interiorPolygon) else { continue }
path.addPath(interiorPath, transform: .identity)
}
}
let startPoint = point(for: points[0])
path.move(to: CGPoint(x: startPoint.x, y: startPoint.y), transform: .identity)
for i in 1..<pointCount {
let nextPoint = point(for: points[i])
path.addLine(to: CGPoint(x: nextPoint.x, y: nextPoint.y), transform: .identity)
}
return path
}
/// Draws the overlay’s contents at the specified location on the map.
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
// Taken from: http://stackoverflow.com/a/17673411
guard let multiPolygon = self.overlay as? MultiPolygon else { return }
guard let polygons = multiPolygon.polygons else { return }
for polygon in polygons {
guard let path = self.polyPath(for: polygon) else { continue }
self.applyFillProperties(to: context, atZoomScale: zoomScale)
context.beginPath()
context.addPath(path)
context.drawPath(using: CGPathDrawingMode.eoFill)
self.applyStrokeProperties(to: context, atZoomScale: zoomScale)
context.beginPath()
context.addPath(path)
context.strokePath()
}
}
}
Usage - Adding the overlay to your MKMapView
:
// Add the overlay to mapView
let polygonsArray: [MKPolygon] = self.buildMKPolygons()
let multiPolygons = MultiPolygon.init(polygons: polygonsArray)
self.mapView.addOverlay(multiPolygons)
Usage - Implementing the viewForOverlay in your MKMapViewDelegate
:
// Method viewForOverlay:
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MultiPolygon {
let polygonRenderer = MultiPolygonPathRenderer(overlay: overlay)
polygonRenderer.lineWidth = 0.5
polygonRenderer.strokeColor = .mainGreen
polygonRenderer.miterLimit = 2.0
polygonRenderer.fillColor = UIColor.mainGreen.withAlphaComponent(0.2)
return polygonRenderer
}
return MKOverlayRenderer()
}