Why region is changed when I am setting current region? - swift

I have put a button so that add a circle overlay to given point. However I don't know why but while region didn't change my overlay could not be seen.
I couldn't find a func that refresh or reload map. So finally I decided to change map region so slightly that user will not be disturbed. (A little bit zoom out for example).
self.mapView.setRegion(mapView.region, animated: true)
I expect that above code do not change the map region however it does, and also I tried this,
self.mapView.setRegion(MKCoordinateRegion(mapView.visibleMapRect), animated: true)
This also changed the map's region.
What can I do ?
And This is how I add my overlays
func addCircles() {
let center = self.myPinView.center
let origin = self.mapView.convert(center, toCoordinateFrom: mapView)
let overlay1 = MKCircle(center: origin, radius: 3)
let overlay2 = MKCircle(center: origin, radius: 7.5)
let overlay3 = MKCircle(center: origin, radius: 15)
self.mapView.addOverlay(overlay1)
self.mapView.addOverlay(overlay2)
self.mapView.addOverlay(overlay3)
}
And this is my delegate func
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MKCircle {
let circle = MKCircleRenderer(overlay: overlay)
circle.fillColor = circle.fillColor = UIColor(red: 255, green: 0, blue: 0, alpha: 0.6)
circle.strokeColor = .red
return circle
} else {
return MKOverlayRenderer()
}
}

Try to encapsulate them in a dsipatchQueue.main
DispatchQueue.main.async {
self.mapView.setRegion(mapView.region, animated: true)
}

Actually I solve my spesific problem with a different view. My initial problem was, after user pick a position I wanted to draw a circle. However my overlays don't appear until the map region changed. I was trying to draw this circles immediately. And the most user friendly solution that I found, set mapView center to overlay's center.
self.mapView.setCenter(origin, animated: false)
In this way, after user pick a position, immediately map focus on this position and overlays are displayed.

Related

How Do I Draw a String in an MKOverlayRender

The use case I have is one where I want to draw and label counties in a state. Annotations don't seem like the right approach to solve this problem. First of all, the label refers to region rather than a point. Second, there are far too many; so, I would have to selectively show and hide annotations based on zoom level (actually something more like the size of the MKCoordinateRegion span). Lastly, county labels are not all that relevant unless the user starts zooming in.
Just as a side note, county boundaries may be present in map tiles, but they are not emphasized. Moreover, there are a multitude of other boundaries I might want to draw that are completely absent from map tiles.
Ultimately, what I want to do is create an overlay for each county shape (counties are clickable and I can navigate to details) and another set of overlays for the labels. I separate county shapes and labels because county shapes are messy and I just use the center of the county. There is no guarantee with this approach that labels will not draw outside of county shapes, which means labels could end up getting clipped when other counties are drawn.
Drawing the county shapes was relatively easy or at least relatively well documented. I do not include any code on rendering shapes. Drawing text on the other hand is not straight forward, not well documented, and most of the posts on the subject are ancient. The lack of recent posts on the subject as well as the fact that most posts posit solutions that no longer work, use deprecated APIs, or only solve a part of the problem motivates this post. Of course, the lack of activity on this problem could be because my strategy is mind numbingly stupid.
I have posted a complete solution to the problem. If you can improve on the solution below or believe there is a better way, I would appreciate the feedback. Alternatively, if you are trying to find a solution to this problem, you will find this post more helpful than the dozens I have looked at, which on the whole got me to where I am now.
Below is a complete solution that can be run in an Xcode single view Playground. I am running Xcode 14.2. The most important bit of code is the overridden draw function of LabelOverlayRenderer. That bit of code is what I struggled to craft for more than a day. I almost gave up. Another key point is when drawing text, one uses CoreText. The APIs pertaining to drawing and managing text are many and most have had a lot of name changes and deprecation.
import UIKit
import MapKit
import SwiftUI
class LabelOverlayRenderer: MKOverlayRenderer {
let title: String
let center: CLLocationCoordinate2D
init(overlay: LabelOverlay) {
center = overlay.coordinate
title = overlay.title!
super.init(overlay: overlay)
}
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
context.saveGState()
// Set Drawing mode
context.setTextDrawingMode(.fillStroke)
// If I don't do this, the text is upside down.
context.textMatrix = CGAffineTransformMakeScale(1.0, -1.0);
// Text size is crazy big because label has to be miles across
// to be visible.
var attrs = [ NSAttributedString.Key : Any]()
attrs[NSAttributedString.Key.font] = UIFont(name: "Helvetica", size: 128000.0)!
attrs[NSAttributedString.Key.foregroundColor] = UIColor(Color.red)
let attributedString = NSAttributedString(string: title, attributes: attrs)
let line = CTLineCreateWithAttributedString(attributedString)
// Get the size of the whole string, so the string can
// be centered. CGSize is huge because I don't want
// to clip or wrap the string. The range setting
// is just cut and paste. Looks like a place holder.
// Ideally, it is the range of that portion
// of the string for which I want the size.
let frameSetter = CTFramesetterCreateWithAttributedString(attributedString)
let size = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, 0), nil, CGSize(width: 1000000, height: 1000000), nil)
// Center is lat-lon, but map is in meters (maybe? definitely
// not lat-lon). Center string and draw.
var p = point(for: MKMapPoint(center))
p.x -= size.width/2
p.y += size.height/2
// There is no "at" on CTLineDraw. The string
// is positioned in the context.
context.textPosition = p
CTLineDraw(line, context)
context.restoreGState()
}
}
class LabelOverlay: NSObject, MKOverlay {
let title: String?
let coordinate: CLLocationCoordinate2D
let boundingMapRect: MKMapRect
init(title: String, coordinate: CLLocationCoordinate2D, boundingMapRect: MKMapRect) {
self.title = title
self.coordinate = coordinate
self.boundingMapRect = boundingMapRect
}
}
class MapViewCoordinator: NSObject, MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let overlay = overlay as? LabelOverlay {
return LabelOverlayRenderer(overlay: overlay)
}
fatalError("Unknown overlay type!")
}
}
struct MyMapView: UIViewRepresentable {
func makeCoordinator() -> MapViewCoordinator {
return MapViewCoordinator()
}
func updateUIView(_ view: MKMapView, context: Context){
// Center on Georgia
let center = CLLocationCoordinate2D(latitude: 32.6793, longitude: -83.62245)
let span = MKCoordinateSpan(latitudeDelta: 4.875, longitudeDelta: 5.0003)
let region = MKCoordinateRegion(center: center, span: span)
view.setRegion(region, animated: true)
view.delegate = context.coordinator
let coordinate = CLLocationCoordinate2D(latitude: 32.845084, longitude: -84.3742)
let mapRect = MKMapRect(x: 70948460.0, y: 107063759.0, width: 561477.0, height: 613908.0)
let overlay = LabelOverlay(title: "Hello World!", coordinate: coordinate, boundingMapRect: mapRect)
view.addOverlay(overlay)
}
func makeUIView(context: Context) -> MKMapView {
// Create a map with constrained zoom gestures only
let mapView = MKMapView(frame: .zero)
mapView.isPitchEnabled = false
mapView.isRotateEnabled = false
let zoomRange = MKMapView.CameraZoomRange(
minCenterCoordinateDistance: 160000,
maxCenterCoordinateDistance: 1400000
)
mapView.cameraZoomRange = zoomRange
return mapView
}
}
struct ContentView: View {
var body: some View {
VStack {
MyMapView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

custom overlay(renderer) is getting cut of by map tiles (in some cases)

I wrote a custom renderer to represent a min and a max Radius. In some cases the renderer is not working as expected. It looks like the overlay is getting cut of by the map tiles.
See the full video
Here is how I did it. Did I miss something?
class RadiusOverlayRenderer: MKOverlayRenderer {
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
guard let overlay = self.overlay as? RadiusOverlay else {
return
}
let maxRadiusRect = self.rect(for: overlay.boundingMapRect)
.offsetBy(
dx: CGFloat(-overlay.boundingMapRect.height)/2,
dy: CGFloat(-overlay.boundingMapRect.width)/2
)
let minRadiusRect = CGRect(
x: Double(maxRadiusRect.midX)-overlay.minRadRect.width/2,
y: Double(maxRadiusRect.midY)-overlay.minRadRect.height/2,
width: overlay.minRadRect.width,
height: overlay.minRadRect.height)
let aPath = CGMutablePath()
aPath.addEllipse(in: maxRadiusRect)
aPath.addEllipse(in: minRadiusRect)
aPath.closeSubpath()
context.setFillColor(overlay.color.cgColor)
context.setAlpha(overlay.alpha)
context.addPath(aPath)
context.drawPath(using: .eoFillStroke)
}
}
Notice that only the upper left parts are clipped?
with .offsetBy you are drawing outside of the .boundingMapRect.
Remove the .offsetBy...
If you want to draw your circle at a different place, then adjust coordinate and / or boundingMapRect of your MKOverlay.

How can I draw two MKPolygons on MapView without having them connect?

For some reason, when I try to draw two MKPolygons on a mapView (MKMapView) I end up with the two polygons connected. Drawing each polygon individually works fine. And I've verified that each of the polygons don't contain any of the coordinates to form the connection between the two. I've attached an image with the two polygons connected
For reference, here's where I call to add the polygons.
func addPeakTimePolygon(from coordinatesArray: [CLLocationCoordinate2D], title: Int){
let polygon = MKPolygon(coordinates: coordinatesArray, count: coordinatesArray.count)
polygon.title = String(title)
//Should refactor to use .contains(where:
var shouldAdd = true
for polygon in self.currentPolygons{
if polygon.title == String(title){
shouldAdd = false
}
}
if shouldAdd{
self.currentPolygons.append(polygon)
self.mapView.add(polygon)
}
}
And here's my rendererFor code:
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MKPolyline {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = #colorLiteral(red: 0, green: 0.6862745098, blue: 0.7607843137, alpha: 1)
renderer.lineWidth = 5.0
return renderer
}
else if overlay is MKPolygon {
let renderer = MKPolygonRenderer(overlay: overlay)
renderer.fillColor = UIColor.red.withAlphaComponent(0.5)
renderer.strokeColor = UIColor.red
renderer.lineWidth = 2
return renderer
}
return MKOverlayRenderer()
}
It seems like you're making one overlay consisting of two polygons. You can't do that with an MKPolygonRenderer; you will get one polygon, as you are observing.
You will need separate overlays, one for each polygon. Unless you are using iOS 13! In that case, you are in luck: New in iOS 13, multiple polygons or polylines can be combined into an MKMultiPolygon or MKMultiPolyline and drawn by an MKMultiPolygonRenderer or MKMultiPolylineRenderer.
I forgot to check / post the code that was calling addPeakTimePolygon. Here is the problematic code below:
var locationList: [CLLocationCoordinate2D] = []
var title = 0
if let peakTimeCampaignList = data["PeakTimeRewardCampaignList"] as? [[AnyHashable:Any]]{
for campaign in peakTimeCampaignList{
if let polygonPoints = campaign["CampaignPolygon"] as? [[AnyHashable:Any]]{
for polygonPoint in polygonPoints{
let polygonPoint = CLLocationCoordinate2D(latitude: polygonPoint["Latitude"] as! CLLocationDegrees, longitude: polygonPoint["Longitude"] as! CLLocationDegrees)
locationList.append(polygonPoint)
}
}
if let id = campaign["Id"] as? Int{
title = id
}
mapBundle.addPeakTimePolygon(from: locationList, title: title)
}
}
As you can see locationList wasn't being cleared out within the loop, causing whatever we sent over to addPeakTimePolygon to have coordinates from two polygons and MapKit was trying it's best to form a polygon between them.
This was a dumb mistake, but hoping someone else sees this with the same problem!

How to Place Bottom Of a Custom Annotation On a Specific Coordinate in Mapbox iOS

I'm using iOS Mapbox SDK in my app. I changed image for an annotation to a custom image (It looks like a map marker). When I add an annotation to a specific coordinate on the map view, It will be added but the center of my custom annotation image (the marker) will be set on the coordinate. I need to change the marker position to set the bottom of the marker on the coordinate. I found a way but I do not know is there a better way or not?
I converted the coordinate to a point, then changed the point y position, then converted the point to a new coordinate.
func mapView(_ mapView: MGLMapView, imageFor annotation: MGLAnnotation) -> MGLAnnotationImage? {
let reuseIdentifier = "annotationImage"
var annotationImage = mapView.dequeueReusableAnnotationImage(withIdentifier: reuseIdentifier)
if annotationImage == nil {
annotationImage = MGLAnnotationImage(image: UIImage(named: "Orange")!, reuseIdentifier: reuseIdentifier)
}
return annotationImage
}
func addDestinationMarker(coordinate: CLLocationCoordinate2D) {
guard let mapView = mapView else { return }
if let annotations = mapView.annotations {
mapView.removeAnnotations(annotations)
}
var point = mapView.convert(coordinate, toPointTo: mapView)
point.y -= markerImageView.frame.height / 2
let newCoordinate = mapView.convert(point, toCoordinateFrom: mapView)
let annotation = MGLPointAnnotation()
annotation.coordinate = newCoordinate
mapView.addAnnotation(annotation)
}
I've run into this same issue and started to think that round map pins were becoming the defacto standard so they could just be plonked onto the map with the image centre denoting the coordinate. However if you take a look at this example on the Mapbox website, they use a non-round image and solve the offset problem quite nicely.
// The anchor point of an annotation is currently always the center. To
// shift the anchor point to the bottom of the annotation, the image
// asset includes transparent bottom padding equal to the original image
// height.
//
// To make this padding non-interactive, we create another image object
// with a custom alignment rect that excludes the padding.
image = image.withAlignmentRectInsets(UIEdgeInsets(top: 0, left: 0, bottom: image.size.height/2, right: 0))
This does mean that you need to generate pin images that are twice as tall, with the lower half transparent, but that's really not a big deal.
You can solve this by leveraging the centerOffset property that MGLAnnotationView provides. Though I'm not sure if it's present in the MGLAnnotationImage you're using.
To set the anchor to the bottom of the annotation, use:
centerOffset.dy = -height / 2
If you set the frame beforehand, the height is simply frame.height.
The other answers express things correctly but both are missing the correct syntax:
annotationView?.centerOffset.y = -(annotationView?.frame.height ?? 0) / 2
This will achieve the expected result.

Google maps w/ clustering. Check whether a marker has already been rendered

I use Google Maps ios utils clustering and have set up a custom iconView for the marker & cluster like this:
func renderer(_ renderer: GMUClusterRenderer, willRenderMarker marker: GMSMarker) {
// Check if marker or cluster
if marker.userData is PlaceMarker {
if let userData = marker.userData as? PlaceMarker {
marker.iconView = MarkerView(caption: userData.caption)
}
marker.groundAnchor = CGPoint(x: 0.5, y: 1)
marker.isFlat = true
marker.appearAnimation = kGMSMarkerAnimationPop
} else {
// Apply custom view for cluster
marker.iconView = ClusterViewIcon(caption: userData.caption)
// Show clusters above markers
marker.zIndex = 1000;
marker.groundAnchor = CGPoint(x: 0.5, y: 1)
marker.isFlat = true
marker.appearAnimation = kGMSMarkerAnimationPop
}
}
func renderer(_ renderer: GMUClusterRenderer, willRenderMarker marker: GMSMarker) { }
get's called every time there was a zoom level change even if no clustering / declustering happened, and makrer.iconView is always nil there even if it was set up before.
How can one implement a guard to only setup iconView and other marker properties only when the marker is rendered for the first time? otherwise it is just a waste of resources.. (and also the animation happens on every zoom level change)
EDIT: One way I can think of it is to store already rendered markers id in an array and check against it.. but that's a dirty way..
Reference: https://github.com/googlemaps/google-maps-ios-utils/issues/96