How to draw custom shapes on MKMapView
After some RND I came across a library curvyRoute & used it to draw arch on MKMapView
.
// Adds arch overlay to the mapView
private func addArcOverlays() {
let pointA = CLLocationCoordinate2D(latitude: 15.463157486154865, longitude: 73.78846049308775)
let pointB = CLLocationCoordinate2D(latitude: 15.495608080208948, longitude: 73.83418584279791)
mapView.addOverlay(LineOverlay(origin: pointA, destination: pointB))
let style = LineOverlayStyle(strokeColor: .red, lineWidth: 4, alpha: 1)
let arc = ArcOverlay(origin: pointA, destination: pointB, style: style)
arc.radiusMultiplier = 0.5
mapView.addOverlay(arc)
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
switch overlay {
case let lineOverlay as LineOverlay:
let linerender = MapLineOverlayRenderer(lineOverlay)
setVisibleMapRect(linerender.overlay, animated: true)
return linerender
case let polyline as MKPolyline:
let renderer = MKPolylineRenderer(overlay: polyline)
renderer.strokeColor = UIColor.yellow.withAlphaComponent(0.5)
renderer.lineWidth = 4
return renderer
default:
return MKOverlayRenderer()
}
}
How to draw circle overlay on MapKit that surrounds several annotations/coordinates?
I came up with MKCoordinateRegion
initializer, which provides the region of the coordinates, the extension has a computed property to provide the radius of the region.
extension MKCoordinateRegion {
init?(from coordinates: [CLLocationCoordinate2D]) {
guard coordinates.count > 1 else { return nil }
let a = MKCoordinateRegion.region(coordinates, fix: { $0 }, fix2: { $0 })
let b = MKCoordinateRegion.region(coordinates, fix: MKCoordinateRegion.fixMeridianNegativeLongitude, fix2: MKCoordinateRegion.fixMeridian180thLongitude)
guard (a != nil || b != nil) else { return nil }
guard (a != nil && b != nil) else {
self = a ?? b!
return
}
self = [a!, b!].min(by: { $0.span.longitudeDelta < $1.span.longitudeDelta }) ?? a!
}
var radius: CLLocationDistance {
let furthest = CLLocation(latitude: self.center.latitude + (span.latitudeDelta / 2),
longitude: center.longitude + (span.longitudeDelta / 2))
return CLLocation(latitude: center.latitude, longitude: center.longitude).distance(from: furthest)
}
// MARK: - Private
private static func region(_ coordinates: [CLLocationCoordinate2D],
fix: (CLLocationCoordinate2D) -> CLLocationCoordinate2D,
fix2: (CLLocationCoordinate2D) -> CLLocationCoordinate2D) -> MKCoordinateRegion? {
let t = coordinates.map(fix)
let min = CLLocationCoordinate2D(latitude: t.min { $0.latitude < $1.latitude }!.latitude,
longitude: t.min { $0.longitude < $1.longitude }!.longitude)
let max = CLLocationCoordinate2D(latitude: t.max { $0.latitude < $1.latitude }!.latitude,
longitude: t.max { $0.longitude < $1.longitude }!.longitude)
// find span
let span = MKCoordinateSpanMake(max.latitude - min.latitude, max.longitude - min.longitude)
// find center
let center = CLLocationCoordinate2D(latitude: max.latitude - span.latitudeDelta / 2,
longitude: max.longitude - span.longitudeDelta / 2)
return MKCoordinateRegion(center: fix2(center), span: span)
}
private static func fixMeridianNegativeLongitude(coordinate: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
guard (coordinate.longitude < 0) else { return coordinate }
let fixedLng = 360 + coordinate.longitude
return CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: fixedLng)
}
private static func fixMeridian180thLongitude(coordinate: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
guard (coordinate.longitude > 180) else { return coordinate }
let fixedLng = -360 + coordinate.longitude
return CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: fixedLng)
}
}
Usage:
let coordinates: [CLLocationCoordinate2D] = self.mapView.annotations.map{ $0.coordinate }
if let region = MKCoordinateRegion(from: coordinates) {
self.mapView.add(MKCircle(center: region.center, radius: region.radius))
}
Result is exactly what I want, with ability to handle coordinates crossing 180th meridian:
Is there any way to move MKCircle overlay with MKUserLocation without flicking or blinking?
There are two options:
I’ve found that, in general, the flickering effect is diminished if you add the new overlay before removing the old one.
You might consider making the circle an custom annotation view rather than an overlay. That way, you can just adjust the
coordinate
without adding/removing.
By putting the circle in the annotation, itself, it’s seamless, both with user tracking on:
I turned off user tracking half way through, so you could see both patterns.
class CirclePointerAnnotationView: MKAnnotationView {
let circleShapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.lightGray.withAlphaComponent(0.25).cgColor
shapeLayer.strokeColor = UIColor.clear.cgColor
return shapeLayer
}()
let pinShapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.blue.cgColor
shapeLayer.strokeColor = UIColor.clear.cgColor
return shapeLayer
}()
let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage(named: "woman")
imageView.clipsToBounds = true
return imageView
}()
var pinHeight: CGFloat = 100
var pinRadius: CGFloat = 30
var annotationViewSize = CGSize(width: 300, height: 300)
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
layer.addSublayer(circleShapeLayer)
layer.addSublayer(pinShapeLayer)
addSubview(imageView)
bounds.size = annotationViewSize
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
let radius = min(bounds.width, bounds.height) / 2
let center = CGPoint(x: bounds.midX, y: bounds.midY)
circleShapeLayer.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
let angle = asin(pinRadius / (pinHeight - pinRadius))
let pinCenter = CGPoint(x: center.x, y: center.y - (pinHeight - pinRadius))
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: pinCenter, radius: pinRadius, startAngle: .pi - angle, endAngle: angle, clockwise: true)
path.close()
pinShapeLayer.path = path.cgPath
let imageViewDimension = pinRadius * 2 - 15
imageView.bounds.size = CGSize(width: imageViewDimension, height: imageViewDimension)
imageView.center = pinCenter
imageView.layer.cornerRadius = imageViewDimension / 2
}
}
How to draw UIBezierPath overlay on MKMapView?
According to Anna's answer you should use [self pointForMapPoint:points[i]]
instead of points[i]
- (void)createPath
{
MKPolyline *line = (id)self.overlay;
MKMapPoint *points = line.points;
NSUInteger pointCount = line.pointCount;
CGMutablePathRef path = CGPathCreateMutable();
CGPoint point = [self pointForMapPoint:points[0]];
CGPathMoveToPoint(path, NULL, point.x, point.y);
for (int i = 1; i < pointCount; i++) {
point = [self pointForMapPoint:points[i]];
CGPathAddLineToPoint(path, NULL, point.x, point.y);
}
[self setPath:path];
}
Related Topics
Swift Spritekit Get Visible Frame Size
Why Are Image Views Sometimes Not Appearing in Collection View Cells
Sklabelnode Text with Two Different Fonts and Colour. How Is This Possible
It Is Posible to Load Customise HTML View into Webview in Swift
How to Show an Alert in Swift UIalertview Not Working
Higlight Speech Utterance in Swiftui
How to Determine Whether a Double Is an Integer
Swift Corebluetooth Reading a Float Array from Ble
Swift's Decimal Precision Issue
Read UId from Nfc Mifare Tag iOS 13
Validate Unicode Code Point in Swift
Uialertview or UIalertcontroller to Display Only Once in Swift
How to Set Window Level in Swift
Data Structure for Fast Lookup with Multiple Criteria
Why Do I Keep Getting The Error "No Such Module 'Realmswift'"