I'm displaying custom annotations over the map and having hard times to receive didSelect calls on my delegate. Here is the code of the ViewController:
class TestAnnotationClickViewController: UIViewController, MGLMapViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let mapView = MGLMapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.delegate = self
mapView.addAnnotation(TestAnnotation())
view.addSubview(mapView)
}
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
if annotation is TestAnnotation {
let view = TestAnnotationView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
return view
}
return nil
}
func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
print("annotation didSelect")
}
func mapView(_ mapView: MGLMapView, didSelect annotationView: MGLAnnotationView) {
print("annotation view didSelect")
}
}
Here is the code for annotation class and corresponding view:
class TestAnnotation: NSObject, MGLAnnotation {
var coordinate: CLLocationCoordinate2D
override init() {
coordinate = CLLocationCoordinate2D(latitude: 33.9415889, longitude: -118.4107187)
}
}
class TestAnnotationView: MGLAnnotationView {
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
private func setupView() {
backgroundColor = .green
}
}
When I'm pressing the annotation (green rectangle) I expect delegate methods didSelect to be called. However neither of them is invoked. And the console doesn't get "annotation didSelect" or "annotation view didSelect" printed.
I also tried to set isUserInteractionEnabled on the TestAnnotationView but it didn't help. What am I missing?
I install Mapbox (5.9.0) via cocoapods:
pod 'Mapbox-iOS-SDK', '~> 5.9'
I tend to use reuseIdentifiers for the creation of annotations and construct an init that carries that and the annotation through so for your use-case, something like:
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
if annotation is TestAnnotation {
let view = TestAnnotationView(reuseIdentifier: "test", frame: CGRect(x: 0, y: 0, width: 100, height: 100), annotation: annotation)
return view
}
return nil
}
and in the TestAnnotationViewClass adding the initialiser:
init(reuseIdentifier: String?, frame: CGRect, annotation: MGLAnnotation) {
super.init(reuseIdentifier: reuseIdentifier)
self.frame = frame
setupView()
}
ensures that everything is setup so the annotations can respond to touches and trigger the didSelect delegate methods.
Related
I have a problem with clustering in MapKit, it doesn't work properly at start, I have to pan some time to force MapKit to "recalculate" clustering. The video below shows the problem.
https://youtu.be/5PK7uAV0F_8
My code's description
I have a class class MapKitViewController: UIViewController, MKMapViewDelegate with methods
override func loadView() {
super.loadView()
mapView = MKMapView(frame: view.bounds)
mapView.delegate = self
view.addSubview(mapView)
mapView.translatesAutoresizingMaskIntoConstraints = false
mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
mapView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
private func registerAnnotationViewClasses() {
mapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
mapView.register(ClusterAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)
}
override func viewDidLoad() {
super.viewDidLoad()
registerAnnotationViewClasses()
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MyAnnotation {
let v = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
v.titleVisibility = .hidden
v.clusteringIdentifier = "clusterID"
return v
} else {
return nil
}
}
And this is my cluster annotation view class
class ClusterAnnotationView: MKAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
setUp()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp()
}
private func setUp() {
collisionMode = .circle
}
override func prepareForDisplay() {
super.prepareForDisplay()
displayPriority = .defaultHigh
if let clusterAnnotation = annotation as? MKClusterAnnotation {
image = image(forMembersCount: clusterAnnotation.memberAnnotations.count)
}
}
private func image(forMembersCount membersCount: Int) -> UIImage {
let size = /* calculate size */
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { _ in
/* draw an image */
}
}
}
What can I do to fix this issue?
in
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MyAnnotation {
let v = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
v.titleVisibility = .hidden
v.clusteringIdentifier = "clusterID"
return v
} else {
return nil
}
}
you are recreating all annotationViews, probably each time you are panning.
These are intended to be reused instead of recreated each time.
So mapView thinks you are replacing your annotationViews with new objects (that's what you are actually doing). This means there can't be a smooth transition of existing views - the old views don't exist any more.
See https://www.raywenderlich.com/7738344-mapkit-tutorial-getting-started#toc-anchor-009
after the title "Configuring the Annotation View"
on how to reuse annotationViews.
There are approximately 1500 customized annotations with values and images.
I understand that this customization has a high price to be rendered in real time, but the UI is freezing(some time) during the viewfor annotation (when the user moves the map, each annotation takes time to render and more it zooms out, more annotations appear and the longer the UI freezes).
In the same application on android, I managed to improve this by clustering all the annotations out of sight, only rendering them when they became visible, without freezing the map.
Rendering must occur in the background, outside the main thread.
Other suggestions are also welcome.
Some code: viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
mapView.register(CustomPinAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
// mapView.register(CustomPinAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)
... configureDatabase()
... loadMapMap()
... loadAnnotations()
}
more code: viewfor annotation
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation.isMember(of: MKUserLocation.self) { return nil }
if annotation is MKClusterAnnotation { return nil }
guard let customAnnotation = annotation as? CustomPin else { return nil }
... GET ANNOTATION INFO ...
let reusePin = preco+stared+fechado+excluido+publico+particular
var pinView = mapView.dequeueReusableAnnotationView(withIdentifier: reusePin) as? CustomPinAnnotationView
if pinView == nil { pinView = CustomPinAnnotationView(annotation: annotation, reuseIdentifier: reusePin) }
else { return pinView }
... RENDER CUSTOM ANNOTATION IMAGE ...
UIGraphicsEndImageContext()
pinView?.canShowCallout = true
pinView?.centerOffset = CGPoint(x: size.width / 5 , y: -size.height / 5)
print("=", terminator:"")
return pinView
}
CustomPinAnnotationView.swift
import Foundation
import UIKit
import MapKit
class CustomPinAnnotationView: MKAnnotationView {
private var observerContext = 0
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
clusteringIdentifier = "cluster"
collisionMode = .circle
centerOffset = CGPoint(x: 0, y: -10) // Offset center point to animate better with marker annotations
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var annotation: MKAnnotation? {
willSet {
removeObserverIfAny()
}
didSet {
if let annotation = annotation as? CustomPin {
annotation.addObserver(self, forKeyPath: #keyPath(CustomPin.subtitle), context: &observerContext)
}
}
}
deinit {
removeObserverIfAny()
}
private func removeObserverIfAny() {
if let oldAnnotation = annotation as? CustomPin {
oldAnnotation.removeObserver(self, forKeyPath: #keyPath(CustomPin.subtitle))
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard context == &observerContext else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
}
}
I am trying to create a polygon on my map to show a boundry. But when I run my app nothing appears. I can see the map but not the polygon What's my error? thanks.
import UIKit
import MapKit
import CoreLocation
class MapScreen : UIViewController {
#IBOutlet var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
mapView.delegate = self as! MKMapViewDelegate
// center the location on the screen
var location = CLLocationCoordinate2DMake(41.308792, -72.928641)
var span = MKCoordinateSpanMake(0.01, 0.01)
var region = MKCoordinateRegionMake(location, span)
mapView.setRegion(region, animated: true)
//calling the method
addBoundry()
}
func addBoundry(){ //creation of a polygon
var points = [CLLocationCoordinate2DMake(41.311674, -72.925506),
CLLocationCoordinate2DMake(41.307308, -72.928694),
CLLocationCoordinate2DMake(41.307108, -72.928324),
CLLocationCoordinate2DMake(41.307892, -72.930285),
CLLocationCoordinate2DMake(41.307892, -72.931223),
CLLocationCoordinate2DMake(41.307227, -72.932494),
CLLocationCoordinate2DMake(41.308452, -72.931663),
CLLocationCoordinate2DMake(41.308730, -72.932773),
CLLocationCoordinate2DMake(41.308496, -72.931614),
CLLocationCoordinate2DMake(41.308496, -72.931614),
CLLocationCoordinate2DMake(41.311288, -72.931630),
CLLocationCoordinate2DMake(41.311659, -72.930945),
CLLocationCoordinate2DMake(41.312893, -72.932153),
CLLocationCoordinate2DMake(41.313433, -72.930542),
CLLocationCoordinate2DMake(41.313324, -72.929963),
CLLocationCoordinate2DMake(41.312758, -72.929027),
CLLocationCoordinate2DMake(41.312373, -72.927167),
CLLocationCoordinate2DMake(41.311674, -72.925506)]
let polygon = MKPolygon(coordinates: &points, count: points.count)
mapView.add(polygon)
}
func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
if overlay is MKPolygon {
let polygonView = MKPolygonRenderer(overlay: overlay)
polygonView.fillColor = UIColor(red: 0, green: 0.847, blue: 1, alpha: 0.25)
return polygonView
}
return nil
}
}
Your signature for mapView(_:rendererFor:) is incorrect. In Swift 3, it would be:
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
...
}
If you formally declare your conformance to protocols like MKMapViewDelegate, it will often warn you about these things. For example, best practice would be do so in a class extension rather than in the class definition itself:
extension MapScreen: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
...
}
}
By the way, if you do that, not only do you get warnings about protocol conformance, but you also don't need to cast it when setting the delegate:
mapView.delegate = self
I implemented map view and created simple data model for annotations.
The pins are shown on the map, but I am still unable to get the details by tapping any pin.
My Code:
import UIKit
import MapKit
class MapViewController: UIViewController, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
#IBOutlet weak var topView: MapDataView!
var dummyModel: DummyModel?
override func viewDidLoad() {
super.viewDidLoad()
dummyModel = DummyModel()
mapView.delegate = self
mapView.showsUserLocation = true
let region = MKCoordinateRegionMakeWithDistance(mapView.userLocation.coordinate, 1000, 1000)
mapView.setRegion(region, animated: true)
let range = 0..<Int((dummyModel?.objectsArray.count)!)
Array is valid and has no nil:
for i in range {
mapView.addAnnotation((dummyModel?.objectsArray[i])!)
}
The way i add annotation to the map:
mapView.selectAnnotation(mapView.annotations[0], animated: true)
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
print("viewForAnnotation")
let identifier = "PubMapObject"
if annotation is PubMapObject {
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if annotationView == nil {
annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView!.canShowCallout = true
let btn = UIButton(type: .detailDisclosure)
annotationView!.rightCalloutAccessoryView = btn
} else {
annotationView!.annotation = annotation
}
return annotationView
}
return nil
}
This method never called and i dont know why. i guess the problem is here, but i am unable to find it ):
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
print("annotationView")
let pub = view.annotation as! PubMapObject
}
}
Try to add set frame of annotation view before returning annotation view
//my annotation class and provide width and height to annotation view before returning the annotation view and give frame to
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if (annotation.isKind(of: MKUserLocation.self)) {
return nil
}
let reuseId = "PubMapObject"
if (annotation.isKind(of: PubMapObject.self)) {
let anView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
anView.isEnabled = true
anView.isUserInteractionEnabled = true
let btn = UIButton(type: .detailDisclosure)
btn.frame = CGRect(x: 0, y: 0, width: 50, height: 70)
anView!.rightCalloutAccessoryView = btn
anView.frame = CGRect(x: 0, y: 0, width: 50, height: 70)
return anView
}
return nil
}
Try using didSelect function instead of calloutAccessoryControlTapped.
func mapView(_ mapView: MKMapView, didSelect annotationView: MKAnnotationView) { ...
What I want to do is present a popover with the annotation as its source view/anchor
By using delegate function didSelect annotationView I should be able to achieve this, but it doesn't seem to run at all.
For now I'm just presenting my popover via didSelect annotation and have set the sourceView to nav bar, just for showing it somewhere...
FYI:
I have implemented the Mapbox SDK into the project.
I have no problems performing the same task using MapKit.
Does anyone have any idea on what I can do to achieve this?
code snippets below:
import UIKit
import Mapbox
class ViewController: UIViewController, MGLMapViewDelegate, UIPopoverPresentationControllerDelegate {
#IBOutlet var theMap: MGLMapView!
override func viewDidLoad() {
super.viewDidLoad()
theMap.delegate = self
let point = MGLPointAnnotation()
point.coordinate = CLLocationCoordinate2D(latitude: 55.6, longitude: 13.0)
point.title = "Some place"
point.subtitle = "Malmö, Sweden"
theMap.addAnnotation(point)
}
func mapView(_ mapView: MGLMapView, didSelect annotationView: MGLAnnotationView) {
print("annotation view: ", annotationView)
// this method doesn't seem to get called at all...
// but ideally this is the place to present the popover.
}
func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
print("annotation: ", annotation)
// present the popover
presentPopover()
}
func presentPopover(){
let popover = storyboard?.instantiateViewController(withIdentifier: "MyCalloutVC") as! MyCallout
popover.modalPresentationStyle = UIModalPresentationStyle.popover
popover.popoverPresentationController?.backgroundColor = UIColor.white
popover.popoverPresentationController?.delegate = self
// I would like to set the source anchor to the selected annotation view.
popover.popoverPresentationController?.sourceView = UINavigationBar() // set to nav bar for now...
popover.popoverPresentationController?.permittedArrowDirections = .any
// popover size set in MyCallout
self.present(popover, animated: true)
}
}
The function does not get called because there is no annotation view, which can be selected.
It means that you simply add an point annotation and not an annotation view. Therefore you will have to do this:
When you add the annotation the func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {} gets called, where you can define an annotation view an return it. Then the did selected function will be called. Have a look at the example code : https://www.mapbox.com/ios-sdk/examples/annotation-views/
import Mapbox
// Example view controller
class ViewController: UIViewController, MGLMapViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let mapView = MGLMapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.styleURL = MGLStyle.darkStyleURL(withVersion: 9)
mapView.tintColor = .lightGray
mapView.centerCoordinate = CLLocationCoordinate2D(latitude: 0, longitude: 66)
mapView.zoomLevel = 2
mapView.delegate = self
view.addSubview(mapView)
// Specify coordinates for our annotations.
let coordinates = [
CLLocationCoordinate2D(latitude: 0, longitude: 33),
CLLocationCoordinate2D(latitude: 0, longitude: 66),
CLLocationCoordinate2D(latitude: 0, longitude: 99),
]
// Fill an array with point annotations and add it to the map.
var pointAnnotations = [MGLPointAnnotation]()
for coordinate in coordinates {
let point = MGLPointAnnotation()
point.coordinate = coordinate
point.title = "\(coordinate.latitude), \(coordinate.longitude)"
pointAnnotations.append(point)
}
mapView.addAnnotations(pointAnnotations)
}
// MARK: - MGLMapViewDelegate methods
// This delegate method is where you tell the map to load a view for a specific annotation. To load a static MGLAnnotationImage, you would use `-mapView:imageForAnnotation:`.
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
// This example is only concerned with point annotations.
guard annotation is MGLPointAnnotation else {
return nil
}
// Use the point annotation’s longitude value (as a string) as the reuse identifier for its view.
let reuseIdentifier = "\(annotation.coordinate.longitude)"
// For better performance, always try to reuse existing annotations.
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
// If there’s no reusable annotation view available, initialize a new one.
if annotationView == nil {
annotationView = CustomAnnotationView(reuseIdentifier: reuseIdentifier)
annotationView!.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
// Set the annotation view’s background color to a value determined by its longitude.
let hue = CGFloat(annotation.coordinate.longitude) / 100
annotationView!.backgroundColor = UIColor(hue: hue, saturation: 0.5, brightness: 1, alpha: 1)
}
return annotationView
}
func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
return true
}
}
//
// MGLAnnotationView subclass
class CustomAnnotationView: MGLAnnotationView {
override func layoutSubviews() {
super.layoutSubviews()
// Force the annotation view to maintain a constant size when the map is tilted.
scalesWithViewingDistance = false
// Use CALayer’s corner radius to turn this view into a circle.
layer.cornerRadius = frame.width / 2
layer.borderWidth = 2
layer.borderColor = UIColor.white.cgColor
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Animate the border width in/out, creating an iris effect.
let animation = CABasicAnimation(keyPath: "borderWidth")
animation.duration = 0.1
layer.borderWidth = selected ? frame.width / 4 : 2
layer.add(animation, forKey: "borderWidth")
}
}