I have been testing out using MapBox for my SwiftUI application but I've run into issues where my annotations are not appearing on my MapView even though they appear to have been added. My code for my updateAnnotations command, which is called on updateUIView is:
private func updateAnnotations() {
if let currentAnnotations = mapView.annotations {
mapView.removeAnnotations(currentAnnotations)
}
for marker in devices.positions {
let annotation = MGLPointAnnotation(title: marker.name, coordinate: marker.coordinate)
mapView.addAnnotation(annotation)
print("ADDED \(annotation)")
print(mapView.annotations)
}
}
devices is a #ObservedObject containing a positions array of points with a title and coordinate to plot. My output from this code is:
ADDED <MGLPointAnnotation: 0x285abc9f0; title = "Test"; subtitle = (null); coordinate = -36.892800, 174.625000>
Optional([<MGLPointAnnotation: 0x2866b7c60; title = "Test"; subtitle = (null); coordinate = -36.892800, 174.625000>])
This doesn't make sense to me - the output suggests that the annotation has been created but it doesn't show up on the map. Any help would be greatly appreciated.
You need to pass in the MapView when using SwiftUI - e.g. change updateAnnotations() to updateAnnotations(_ mapView: MGLMapView)
Related
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()
}
}
I have a mapView in one view controller with an MKPointAnnotation. I've applied reverse Geocoder to get the street name from the coordinates and I'm trying to take that street name onto another view controller and place it into a label. But that label from the second view controller always shows the coordinates 0.0, instead of the street name.
func configureAddressNameTry() {
let vc = ViewController()
let lat = vc.annotation.coordinate.latitude
let long = vc.annotation.coordinate.longitude
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(CLLocation(latitude: lat, longitude: long)) { (placemarks, error) in
if let places = placemarks {
for place in places {
// This is the line that gives me the coordinates 0.0 (North Atlantic Ocean)
self.addressNameTry.text = place.name
}
}
}
I think I have all the necessary code for the user location and the information in the info.plist, so I don't know what I'm missing. I'm new to programming and I hope I'm not making a basic error.
Thank you for your replies :)
I need to do something like this
a marker with a static label in google maps sdk for iOS
If there is gonna be a lot of markers, I do not recommend using iconView because it makes UI so laggy, but here it goes:
Create a UIView file as "MarkerInfoView", which will be created as MarkerInfoView.xib
Then design your UI in there, add your imageView for your icon, then add other necessary views to complete your iconView. Also include marker in the design as an imageView. Because Im not 100% sure but I think you cant use both iconView and icon in google maps.
Then create a swift file called "MarkerInfoView.swift", go to MarkerInfoView.xib and select it's class as MarkerInfoView.
Then create another swift file, lets call it PlaceMarker, inside that file you will create a class which will conform to GMSMarker, then you will initialize your view to set it equal to iconView in PlaceMarker class. Lets do it as following:
class PlaceMarker: GMSMarker {
//Initialize with lat and long, then set position equal to the coordinate.
// 'position' comes from inheriting from GMSMarker, which is google marker.
init(latitude: Double, longitude: Double, distance: Double, placeName: String) {
super.init()
if let lat: CLLocationDegrees = latitude,
let long: CLLocationDegrees = longitude {
let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: long)
position = coordinate
}
let view = Bundle.main.loadNibNamed("MarkerInfoView", owner: nil, options: nil)?.first as! MarkerInfoView
// you can set your view's properties here with data you are sending in initializer.
// Remember if you need to pass more than just latitude and longitude, you need
// to update initializer.
// lets say you created 2 outlet as placeNameLabel, and distanceLabel, you can set
// them like following:
view.placeNameLabel.text = placeName
view.distanceLabel.text = distance
// Once your view is ready set iconView property coming from inheriting to
// your view as following:
iconView = view
appearAnimation = .pop //not necessarily but looks nice.
}
}
Then when you have your data and your googlemaps view in a ViewController you can set like:
let latitude = 101.432432 //arbitrary, should come from your source
let longitude = 34.432124
let distance = 4
let placeName = "My place".
let marker = PlaceMarker(latitude: latitude, longitude: longitude, distance: distance, placeName: placeName)
marker.map = self.mapView // your google maps set your marker's map to it.
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.
I am new to iOS and I have a question about finding the current user location. I am reading in Apple documentation:
Displaying the User’s Current Location on the Map Map Kit includes
built-in support for displaying the user’s current location on the
map. To show this location, set the showsUserLocation property of your
map view object to YES. Doing so causes the map view to use Core
Location to find the user’s location and add an annotation of type
MKUserLocation to the map.
The addition of the MKUserLocation annotation object to the map is
reported by the delegate in the same way that custom annotations are.
If you want to associate a custom annotation view with the user’s
location, you should return that view from your delegate object’s
mapView:viewForAnnotation: method. If you want to use the default
annotation view, return nil from that method. To learn more about
adding annotations to a map, see Annotating Maps.
And it sounds great. But then...
import UIKit
import MapKit
class ViewController: UIViewController {
#IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
//set initial location in Honolulu
//let initialLocation = CLLocation(latitude: 21.282778, longitude: -157.829444)
mapView.showsUserLocation = true
let initialLocation = mapView.userLocation.location
centerMapOnLocation(initialLocation)
// let artwork = Artwork(title: "King David Kalakaua", locationName: "Waikiki Gateway Park", discipline: "Sculpture", coordinate: CLLocationCoordinate2D(latitude: 21.283921, longitude: -157.831661))
// mapView.addAnnotation(artwork)
//
// mapView.delegate = self
}
let regionRadius: CLLocationDistance = 1000
func centerMapOnLocation(location: CLLocation){
let coordinateRegion = MKCoordinateRegionMakeWithDistance(location.coordinate, regionRadius * 2.0, regionRadius * 2.0)
mapView.setRegion(coordinateRegion,animated:true)
}
}
And I have a fatal error: unexpectedly found nil while unwrapping an Optional value. I don't get why. If I set the location manually - of course it is fine. But in the documentation it is written that it will add an annotation on the map. No annotation is added and it crashes. Isn't it possible to get the user coordinates without using the CLLocationManager?
Have you asked permissions to the user to let your app use the location services? The docs at Apple can help you with this. Look at the sample code below to get you started:
private func beginLocationUpdates() {
// Request location access permission
_locationManager!.requestWhenInUseAuthorization()
// Start observing location
_locationManager!.startUpdatingLocation()
}