MKPolyLine Swift 4 - swift

I am trying to learn MapKit and add some MKPolyLine as an overlay. Couple of questions:
What is the difference between MKPolyLine and MKPolyLineView. Which one should be used when?
For the MKPolyLine init method one of the parameter is a generic type(MKMapPoint) of UnsafePointer? Not really sure what is that supposed to mean. Looking up the various SO questions, it seems we are supposed to pass the memory address of the CLLocationCoordinate2D struct as a parameter, but it doesn't work for me.
let testline = MKPolyline()
let coords1 = CLLocationCoordinate2D(latitude: 52.167894, longitude: 17.077399)
let coords2 = CLLocationCoordinate2D(latitude: 52.168776, longitude: 17.081326)
let coords3 = CLLocationCoordinate2D(latitude: 52.167921, longitude: 17.083730)
let testcoords:[CLLocationCoordinate2D] = [coords1,coords2,coords3]
let line = MKPolyline.init(points: &testcoords, count: testcoords.count)
mapView.add(testline)
I keep getting a "& is not allowed passing array value as 'UnsafePointer<MKMapPoint>" argument".
What is wrong here?

1.
MKPolyLine is a class which holds multiple map-coordinates to define the shape of a polyline, very near to just an array of coordinates.
MKPolyLineView is a view class which manages the visual representation of the MKPolyLine. As mentioned in the Kane Cheshire's answer, it's outdated and you should use MKPolyLineRenderer.
Which one should be used when?
You need to use both MKPolyLine and MKPolyLineRenderer as in the code below.
2.
MKPolyLine has two initializers:
init(points: UnsafePointer<MKMapPoint>, count: Int)
init(coordinates: UnsafePointer<CLLocationCoordinate2D>, count: Int)
When you want to pass [CLLocationCoordinate2D] to MKPolyLine.init, you need to use init(coordinates:count:).
(Or you can create an Array of MKMapPoint and pass it to init(points:count:).)
And when you want to pass an immutable Array (declared with let) to UnsafePointer (non-Mutable), you have no need to prefix &.
The code below is actually built and tested with Xcode 9 beta 3:
import UIKit
import MapKit
class ViewController: UIViewController, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let coords1 = CLLocationCoordinate2D(latitude: 52.167894, longitude: 17.077399)
let coords2 = CLLocationCoordinate2D(latitude: 52.168776, longitude: 17.081326)
let coords3 = CLLocationCoordinate2D(latitude: 52.167921, longitude: 17.083730)
let testcoords:[CLLocationCoordinate2D] = [coords1,coords2,coords3]
let testline = MKPolyline(coordinates: testcoords, count: testcoords.count)
//Add `MKPolyLine` as an overlay.
mapView.add(testline)
mapView.delegate = self
mapView.centerCoordinate = coords2
mapView.region = MKCoordinateRegion(center: coords2, span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02))
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
//Return an `MKPolylineRenderer` for the `MKPolyline` in the `MKMapViewDelegate`s method
if let polyline = overlay as? MKPolyline {
let testlineRenderer = MKPolylineRenderer(polyline: polyline)
testlineRenderer.strokeColor = .blue
testlineRenderer.lineWidth = 2.0
return testlineRenderer
}
fatalError("Something wrong...")
//return MKOverlayRenderer()
}
}

Change testCoords to be var instead of let. It needs to be mutable (variable) to be passed in as a pointer.
You're also trying to pass an array of CLLocationCoordinate2Ds into an argument that expects an array of MKMapPoints. Either convert your coordinates into points or use the function that takes an array of coordinates instead.
MKPolyLineView is an old way of rendering lines onto maps, Apple's docs say to use MKPolylineRenderer from iOS 7 and onwards: https://developer.apple.com/documentation/mapkit/mkpolylineview

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()
}
}

Swift IOS Overlay

My IOS map isn't rendering the NOAA tiles on top of it. Can someone give my a clue what is wrong? Here is my viewcontroler. It's just a standard mapkit viewer. Anyways, it is not working and is just giving me a standard satellite map when I run this code. Does anyone know what I am doing wrong?
Thanks for any help you can give me.
Have a great rest of your weekend.
import UIKit
import MapKit
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet weak var mapView: MKMapView!
//Linking MapView
override func viewDidLoad() {
super.viewDidLoad()
self.mapView.delegate = self
self.mapView.mapType = MKMapType.satellite
let centre = CLLocationCoordinate2D(latitude: 39.2189, longitude: -76.0690)
let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
let region = MKCoordinateRegion(center: centre, span: span)
self.mapView.setRegion(region, animated: false)
self.mapView.regionThatFits(region)
let template = "https://tileservice.charts.noaa.gov/tiles/50000_1/{z}/{x}/{y}.png"
let carte_indice = MKTileOverlay(urlTemplate:template)
carte_indice.isGeometryFlipped = true
carte_indice.canReplaceMapContent = false
self.mapView.addOverlay(carte_indice)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func mapView(
mapView: MKMapView!, rendererFor overlay: MKOverlay!) -> MKOverlayRenderer!
{
if overlay is MKTileOverlay{
var renderer = MKTileOverlayRenderer(overlay:overlay)
renderer.alpha = 0.8
return renderer
}
return nil
}
}
After a lot of mulling over it, I solved my issue. Posting the solution if anyone needs it.
Thanks to GJ Nilsen for helping me figure this out. The problem was that the NOAA tiles didn't have coverage for everywhere, so I solved it by simplify forcing it to load.
if overlay is MKTileOverlay{
var renderer = MKTileOverlayRenderer(overlay:overlay)
renderer.alpha = 0.8
return renderer
}
else{
var renderer = MKTileOverlayRenderer(overlay:overlay)
renderer.alpha = 0.8
return renderer
}
The NOAA RNC Map Tile server is being taken out of commission, and some of the tiles my app uses started being disrupted around the same time you posted this question. This is part of the larger transfer from Raster to Electronic charting, and NOAA's discontinuation of paper charts.
https://nauticalcharts.noaa.gov/updates/coast-survey-to-shut-down-the-raster-navigational-chart-tile-service-and-other-related-services/
I am working to find a similar solution, but all of the new services I have found require a pay model of some sort (MBTile conversion, ArcGIS, etc) ... I miss the good old free NOAA map tiles!

Swift annotation location update

import UIKit
import MapKit
import CoreLocation
class ServisimNeredeViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {
var coordinates: [[Double]]!
var names:[String]!
var addresses:[String]!
var phones:[String]!
var locationManager :CLLocationManager = CLLocationManager()
let singleton = Global.sharedGlobal
let point = ServisimAnnotation(coordinate: CLLocationCoordinate2D(latitude: 41.052466 , longitude: 29.132123))
override func viewDidLoad() {
super.viewDidLoad()
coordinates = [[41.052466,29.108976]]// Latitude,Longitude
names = ["Servisiniz Burada"]
addresses = ["Furkan Kutlu"]
phones = ["5321458375"]
self.map.delegate = self
let coordinate = coordinates[0]
point.image = UIImage(named: "direksiyon")
point.name = names[0]
point.address = addresses[0]
point.phone = phones[0]
self.map.addAnnotation(point)
...
}
...
}
I add the coordinate's annotation when the first screen is loaded I update the newly set coordination when the button is pressed. I want instant update of location when button is pressed. How can I do it?
#IBAction func tttesttt(_ sender: Any) {
self.point.coordinate = CLLocationCoordinate2D(latitude: 42.192846, longitude: 29.263417)
}
Does not update the new location when you do the above operation. But the coordination is eliminated and the new one is updated instead I did it like this, but it did not happen again
DispatchQueue.main.async {
self.point.coordinate = CLLocationCoordinate2D(latitude: surucuKordinant.latitude!, longitude: surucuKordinant.longitude!)
}
The likely issue is that your configuration property has not been configured for key-value observing (KVO), which is how the map and/or annotation view become aware of changes of coordinates.
I would ensure that coordinate is KVO-capable by including the dynamic keyword. For more information, see the Key-Value Observing in Using Swift with Cocoa and Objective-C: Adopting Cocoa Design Patterns.
Clearly, we don't have to do all of that observer code in that KVO discussion (as MapKit is doing all of that), but we do at least need to make our annotation KVO-capable. For example, your annotation class might look like:
class ServisimAnnotation: NSObject, MKAnnotation {
dynamic var coordinate: CLLocationCoordinate2D
...
}
The failure to declare coordinate as dynamic will prevent key-value notifications from being posted, and thus changes to the annotation will not be reflected on the map.

How to update a GMSMapView with GMSCameraUpdate

I call animateWithCameraUpdate on a GMSMapView expecting it to change the map view to show the new GMSCoordinateBounds but it has no effect.
My map loads in a UICollectionViewReusableView to initially display Western Europe:
#IBOutlet weak var mapView: GMSMapView!
var fetchedResultsController : NSFetchedResultsController!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
let camera = GMSCameraPosition.cameraWithLatitude(51.48, longitude: 0, zoom: 4)
mapView.camera = camera
}
I then call a function to query all my locations and update the GMSMapView to show all my locations:
func plotAll(){
let bounds = GMSCoordinateBounds.init()
for property in fetchedResultsController.fetchedObjects!
{
let capitalAsset : CapitalAsset = property as! CapitalAsset
let marker = GMSMarker.init()
marker.draggable = false
marker.snippet = capitalAsset.address
let location = CLLocationCoordinate2DMake(Double(capitalAsset.latitude!), Double(capitalAsset.longitude!))
marker.position = location
marker.map = mapView
// Update bounds to include marker
bounds.includingCoordinate(marker.position)
}
mapView.animateWithCameraUpdate(GMSCameraUpdate.fitBounds(bounds, withPadding: 50.0))
}
My plotAll function is successfully called and loops through a dozen global locations adding these to the GMSCoordinateBounds.
But the map is not being updated. I was expecting the map view to change when I called animateWithCameraUpdate but it has no effect.
Further information, for debugging I replaced the line
mapView.animateWithCameraUpdate(GMSCameraUpdate.fitBounds(bounds, withPadding: 50.0))
with :
let camera = GMSCameraPosition.cameraWithLatitude(51.48, longitude: 0, zoom: 10)
mapView.camera = camera
This does update my map view so there is no problem with calling my plotAll function, the issue is probably in my use of animateWithCameraUpdate.
I got this to work by replacing :
mapView.animateWithCameraUpdate(GMSCameraUpdate.fitBounds(bounds, withPadding: 50.0))
with:
let camera = mapView.cameraForBounds(bounds, insets:UIEdgeInsetsZero)
mapView.camera = camera;

Using coordinates saved in Core Data to draw on a mapview

I have a list of coordinates stored in Core Data, along with the time at which they were saved, in the following format:
game - Integer 16
latitude - Double
longitude - Double
time - Date
I have managed to take the user's location and store it in Core Data without issue, but I am having trouble retrieving it and displaying it on a map. I have tried looking for a suitable tutorial, but I can't find anything that combines the Core Data and the MKPolyline aspects of my query. I'm quite new to coding in general and I can't quite align the two.
So, I know the following code will help me draw on to the map:
var coordinates = locations.map({ (location: CLLocation) ->
CLLocationCoordinate2D in
return location.coordinate
})
var polyline = MKPolyline(coordinates: &coordinates,
count: locations.count)
And I already have this in my code to get the Core Data:
#IBOutlet weak var mapView: MKMapView!
var locationsList: [Locations] = []
var contextMap = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext!
var requestMap = NSFetchRequest(entityName: "Locations")
let predMap = NSPredicate(format: "game = %d", gameNumber)
requestMap.predicate = predMap
requestMap.sortDescriptors = [NSSortDescriptor(key:"time", ascending: false)]
self.locationsList = context.executeFetchRequest(requestMap, error: nil)! as [Locations]
But I just can't marry the two up in order to take my coordinates from Core Data and put them on the map.
Any help would be great - thanks.
You already have an array of CLLocation coordinates and built a MKPolyline. Everything fine so far.
Now you need to tell the MKMapView to draw that polyline on the map:
...
var polyline = MKPolyline(coordinates: &coordinates, count: locations.count)
// add the overlay
self.mapView.addOverlay(polyLine, level: MKOverlayLevel.AboveLabels)
...
To draw the overlay you need to create a MKOverlayRenderer
func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
if overlay.isKindOfClass(MKPolyline) {
// draw the track
let polyLine = overlay
let polyLineRenderer = MKPolylineRenderer(overlay: polyLine)
polyLineRenderer.strokeColor = UIColor.blueColor()
polyLineRenderer.lineWidth = 2.0
return polyLineRenderer
}
return nil
}