Swift Custom Annotation does not get select - swift

I have a custom annotationView on my map, when I select the custom annotation i want to create a action and pass the annotation data to another view. The best way for it to work I would have to use the built in func didSelect. When i do select the custom annotation nothing thing happen. Im not show why the didSelect function did not get called thank you in advance.
class RquestCustomPointAnnotation:MKPointAnnotation {
var image: String!
}
func placeRquestByUsersOnMap(){
//retrieve item from firebase
var markerArray = [MKPointAnnotation]()
let path = "rquest/frontEnd/posts/userCreatedPost"
self.childRef(path).observe(.childAdded, andPreviousSiblingKeyWith: {snapshot, _ in
//get status
if let status = snapshot.childSnapshot(forPath: "status").value as? String {
if status == "pending"{
let indentifier = "rquest"
if let coordinate = snapshot.childSnapshot(forPath: "coordinate").value as? NSDictionary {
let lat = coordinate["lat"] as! Double
let long = coordinate["long"] as! Double
let location = CLLocationCoordinate2D(latitude: lat, longitude: long)
let point = RquestCustomPointAnnotation()
let rquestView = MKAnnotationView(annotation: point, reuseIdentifier: indentifier)
point.image = "22"
let key = snapshot.key
point.coordinate = location
point.accessibilityValue = key
point.accessibilityLabel = "Rquest"
markerArray.append(point)
self.mapView.addAnnotation(rquestView.annotation!)
//create an obserarver to check if it is
let paths = "rquest/frontEnd/posts/userCreatedPost/\(key)/"
self.childRef(paths).observe(.childChanged, andPreviousSiblingKeyWith: { (snap, _) in
if snap.key == "status" {
if let status = snap.value as? String {
if status != "pending" {
for i in markerArray {
if i.accessibilityValue == key {
self.mapView.removeAnnotation(i)
}
}
}
}
}
})
}
}
}
})
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is RquestCustomPointAnnotation {
let reuseIdentifier = "rquest"
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
annotationView?.canShowCallout = true
} else {
annotationView?.annotation = annotation
}
let customPointAnnotation = annotation as! RquestCustomPointAnnotation
annotationView?.image = UIImage(named: customPointAnnotation.image)
return annotationView
}
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
print("did select")
}

You have canShowCallout set to true. So, when you tap on it, do you want callout or have it call didSelect? (Usually you'd do one or the other, but not both.) And are you seeing your callout?
I notice a curious behavior that if (a) the annotation doesn't have a title, and (b) the annotation view's canShowCallout is true, then not only can it not show the callout, but it also prevents the didSelect from being called.
You may either want to turn canShowCallout to false, or make sure your annotation has a title.

Related

Swift - How to update observed data in a (custom or not) MKAnnotation callout WITHOUT deselecting the annotation

I'm struggling to get KVO updates within a callout already displayed.
My use case: I want to display on an open callout the real time distance between user location and the annotation I add to the map. Annotation does not change its position.
I add annotations to mapView, using a custom annotation I have defined. No issue here.
On each annotation selected, the callout displays all the information defined in the custom annotation
However, the distance is refreshed in the callout ONLY if I unselect the annotation and reselect it
The distance property is declared as #objc dynamic so it can be observed.
I compute the distance each time the user location change. This part works too.
I cannot figure out what I'm missing to have the callout updated without closing and reopening it.
The code I'm using is what is described here by Rob: Swift -How to Update Data in Custom MKAnnotation Callout?
So my question: is it possible to change realtime a value (observed) in a notificationView callout ? If yes is KVO the best approach ?
In the link below, how would be implemented the mapView viewFor method ?
Any example would be very helpful.
It's my first post here, so please if I did it wrong, let me know and I will provide more information and details.
But my situation is trivial: the standard callout performs Key-Value Observation (KVO) on title and subtitle. (And the annotation view observes changes to coordinate.). But how to display change of values in the current open callout ? That is the think I do not get.
CustomAnnotation class:
class CustomAnnotation: NSObject, MKAnnotation {
#objc dynamic var title: String?
#objc dynamic var subtitle: String?
#objc dynamic var coordinate: CLLocationCoordinate2D
#objc dynamic var distance: CLLocationDistance
var poiColor: String?
var poiPhone: String?
init(title: String, subtitle: String, coordinate: CLLocationCoordinate2D, poiColor: String, poiPhone: String, distance: CLLocationDistance) {
self.title = title
self.subtitle = subtitle
self.coordinate = coordinate
self.poiColor = poiColor
self.poiPhone = poiPhone
self.distance = distance
super.init()
}
}
CustomAnnotationView class:
class CustomAnnotationView: MKMarkerAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
displayPriority = .required
canShowCallout = true
detailCalloutAccessoryView = createCallOutWithDataFrom(customAnnotation: annotation as? CustomAnnotation)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
removeAnyObservers()
}
override var annotation: MKAnnotation? {
didSet {
removeAnyObservers()
if let customAnnotation = annotation as? CustomAnnotation {
updateAndAddObservers(for: customAnnotation)
}
}
}
private var subtitleObserver: NSKeyValueObservation?
private var distanceObserver: NSKeyValueObservation?
private let subtitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let distanceLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
}
private extension CustomAnnotationView {
func updateAndAddObservers(for customAnnotation: CustomAnnotation) {
subtitleLabel.text = customAnnotation.subtitle
subtitleObserver = customAnnotation.observe(\.subtitle) { [weak self] customAnnotation, _ in
self?.subtitleLabel.text = customAnnotation.subtitle
}
let locationManager = CLLocationManager()
let theLatitude:CLLocationDegrees = (locationManager.location?.coordinate.latitude)!
let theLongitude:CLLocationDegrees = (locationManager.location?.coordinate.longitude)!
// Get pin location
let pointLocation = CLLocation(latitude: customAnnotation.coordinate.latitude, longitude: customAnnotation.coordinate.longitude)
//Get user location
let userLocation = CLLocation(latitude: theLatitude, longitude: theLongitude)
// Return distance en meters
let distanceFromUser = pointLocation.distance(from: userLocation)
customAnnotation.distance = distanceFromUser*100
distanceLabel.text = String(format: "%.03f", customAnnotation.distance)+" cm"
distanceObserver = customAnnotation.observe(\.distance) { [weak self] customAnnotation, _ in
self?.distanceLabel.text = "\(customAnnotation.distance) cm"
}
}
func removeAnyObservers() {
subtitleObserver = nil
distanceObserver = nil
}
func createCallOutWithDataFrom(customAnnotation: CustomAnnotation?) -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = true
view.addSubview(subtitleLabel)
view.addSubview(distanceLabel)
NSLayoutConstraint.activate([
subtitleLabel.topAnchor.constraint(equalTo: view.topAnchor),
subtitleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor),
subtitleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
subtitleLabel.bottomAnchor.constraint(equalTo: distanceLabel.topAnchor),
distanceLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor),
distanceLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
distanceLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
if let customAnnotation = customAnnotation {
updateAndAddObservers(for: customAnnotation)
}
return view
}
}
And to finish:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }
let annotation = annotation as? CustomAnnotation
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "CustomAnnotation") as? CustomAnnotationView
if annotationView == nil {
annotationView = CustomAnnotationView(annotation: annotation, reuseIdentifier: "CustomAnnotation")
annotationView?.canShowCallout = true
} else {
annotationView?.annotation = annotation
}
return annotationView
}
Thank you.
You would appear to have correctly configured the observers for the subtitle and distance. The problem is that a change in location is not triggering an update to distance. Thus, there is nothing triggering the KVO.
You have an observer for distance, which will trigger an update of the label. But you are not changing distance. You should remove the CLLocationManager code from that routine where you add the observers, and instead create a location manager (not within the annotation view, though) which uses its delegate to update all of the annotation distances, e.g.:
class ViewController: UIViewController {
#IBOutlet weak var mapView: MKMapView!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.distanceFilter = 5
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
}
extension ViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let currentLocation = locations.last(where: { $0.horizontalAccuracy >= 0 }) else { return }
mapView.annotations
.compactMap { $0 as? CustomAnnotation }
.forEach {
$0.distance = CLLocation(latitude: $0.coordinate.latitude, longitude: $0.coordinate.longitude)
.distance(from: currentLocation)
}
}
}
Obviously, you would remove the CLLocationManager code from updateAndAddObservers.

How to let user to add custom annotation?

I could not found any document, video or stackoverflow answer.
Here is my problem. I created map and add into my custom MKAnnotation and MKAnnotationView.
I want to let user to create custom pin and save to it's local via CoreData
MyCustomAnnotation has same attributes which is title, subtitle, and coordinate.
The first solution that I come up with put a button which creates a draggable pin to user location.
But I need to get less complex, more sophistication solution.
private func addPins() {
let list = PrivateLocations.shared.initLocations()
for pin in list {
map.addAnnotation(pin)
}
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
if view.annotation is MKUserLocation { return }
let views = Bundle.main.loadNibNamed("CustomCalloutView", owner: nil, options: nil)
let customView = views?[0] as! CustomCalloutView
customView.delegate = self
customView.isUserInteractionEnabled = true
customView.titleLabel.text = view.annotation?.title!
customView.desc.text = view.annotation?.subtitle!
customView.center = CGPoint(x: view.bounds.size.width / 2, y: -customView.bounds.size.height*0.52)
view.addSubview(customView)
map.setCenter((view.annotation?.coordinate)!, animated: true)
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
return nil
} else {
let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: "CustomAnnotationView")
annotationView.image = UIImage(named: "myImage")
annotationView.canShowCallout = false
return annotationView
}
}
And finally here is my CustomPin class :
var coordinate: CLLocationCoordinate2D
var title: String?
var subtitle: String?
init(_ title: String, _ subtitle: String, _ coordinate: CLLocationCoordinate2D) {
self.title = title
self.subtitle = subtitle
self.coordinate = coordinate
}
That's how I solve this problem,
1) Create a UIView for user show where he wants to add an annotation.
2) Add a pan gesture recognizer in it.
func addPanGesture(view: UIView) {
let pan = UIPanGestureRecognizer(target: self, action: #selector (self.handlePan(sender:)))
view.addGestureRecognizer(pan)
}
3) In my selector func, I call pinDropped() func
#objc func handlePan(sender: UIPanGestureRecognizer) {
let view = sender.view!
let translation = sender.translation(in: self.mapView)
switch sender.state {
case .began, .changed:
pinImage.center = CGPoint(x: dropPinImage.center.x + translation.x, y: dropPinImage.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: view)
break
default:
pinDropped()
break
}
}
4) I write what will be happening in my pinDropped func
func pinDropped() {
DispatchQueue.main.async {
let pin = CustomPin(self.lastOrigin, "pin")
self.mapView.addAnnotation(pin)
}
self.saveButton.alpha = 1
pinImage.alpha = 0
}

Set glyphText of MKMarkerAnnotationView for MKClusterAnnotation

I have a class MapItem which implements MKAnnotation protocol. I am using MKMarkerAnnotationView for displaying Annotations on map.
According to Documentation,
glyphText property of MKMarkerAnnotationView when set to nil, it produces pin image on the marker.
When Clustering the annotation, I want the same pin image on the marker. But system by default sets this to the number of annotations clustered within this cluster.
I even tried setting this property to nil, but has no effect.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if let item = annotation as? MapItem {
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "mapItem") as? MKMarkerAnnotationView
?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "mapItem")
annotationView.annotation = item
annotationView.glyphText = nil
annotationView.clusteringIdentifier = "mapItemClustered"
return annotationView
} else if let cluster = annotation as? MKClusterAnnotation {
let clusterView = mapView.dequeueReusableAnnotationView(withIdentifier: "clusterView") as? MKMarkerAnnotationView
?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "clusterView")
clusterView.annotation = cluster
clusterView.glyphText = nil
return clusterView
} else {
return nil
}
}
This is how I do it:
class POIMarkerClusterView: MKMarkerAnnotationView {
override var annotation: MKAnnotation? {
willSet {
update(newValue: newValue)
}
}
private func update(newValue: MKAnnotation?) {
if let cluster = newValue as? MKClusterAnnotation {
self.image = POIClusterImage(poiStatistics: poiStatistics, count: count)
// MKMarkerAnnotationView's default rendering usually hides our own image.
// so we make it invisible:
self.glyphText = ""
self.glyphTintColor = UIColor.clear
self.markerTintColor = UIColor.clear
}
}
}
This means you can set an arbitrary image that is rendered as the annotation view. I create the image dynamically, but you can just borrow the image from the MKMarkerAnnotationView and set it here, so it looks like the pin that you want.
The main trick is to use UIColor.clear to hide what you don't want to see.

Cluster, MKMapView show many time same annotation

So, I have a swift application (swift 3) who use a map, in this map I have many annotation who gets up to date when user change region of map.
I wanted to use the pod "Cluster", I add it and when I zoom in to show what was in the cluster, the annotations appear several times almost at the same place (some time it form a circle, see picture)
I have already tried to filter my list to get only once instance of annotation in cluster but it doesn't works.
This method is call each time the user change region :
func createAnnotations(pois list:[PlaceModel]) {
poiAnnotations.removeAll()
let filteredAnnotations = mapView.annotations.filter { annotation in
//if annotation is MKUserLocation { return false } // don't remove MKUserLocation
if let temp = annotation.subtitle, let value = temp {
return value == "Place"
}
return false
}
clusterManager.remove(poiAnnotations)
clusterManager.remove(filteredAnnotations)
for poi in list {
let centerLocation = CLLocation(latitude: mapView.centerCoordinate.latitude, longitude: mapView.centerCoordinate.longitude)
if poi.getLocation().distance(from: centerLocation) > Double(Constants.POI_Radius) {
continue
}
let annotation = Annotation()
annotation.coordinate = CLLocationCoordinate2D(latitude: poi.latitude!, longitude: poi.longitude!)
annotation.title = poi.name!
annotation.subtitle = "Place"
annotation.type = .image( Images.MapPins.velhop)
// For first value
if poiAnnotations.isEmpty {
poiAnnotations.append(annotation)
}
for pois in poiAnnotations {
if poiAnnotations.contains(where: {$0.title == poi.name}) {
// Do nothing
} else {
poiAnnotations.append(annotation)
}
}
}
clusterManager.add(poiAnnotations)
}
And Here the place where the clusters and the annotations are created :
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if let annotation = annotation as? ClusterAnnotation {
let identifier = "Cluster"
var view = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
// Type for cluster
let color = UIColor(red: 255/255, green: 149/255, blue: 0/255, alpha: 1)
let type:ClusterAnnotationType = .color(color, radius: 25)
if let view = view as? BorderedClusterAnnotationView {
view.annotation = annotation
view.configure(with: type)
} else {
view = BorderedClusterAnnotationView(annotation: annotation, reuseIdentifier: identifier, type: type, borderColor: .white)
}
return view
} else {
let identifier = "Pin"
var view = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView
if let view = view {
view.annotation = annotation
} else {
view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
}
return view
}
}
I tried several things that did not succeed..
Thanks in advance.

I want to loop over all the annotations from my Cluster Annotation

I'm using the cocoapod FBAnnotationClusteringSwift and it's possible to group my annotations together. However I want to loop over all those annotations that are clustered together when the cluster annotation is tapped.
How do I do this?
func mapView(mapView: MKMapView, didSelectAnnotationView view: MKAnnotationView) {
if (view.annotation!.isKindOfClass(FBAnnotationCluster) == true){
//I WANT TO LOOP OVER ALL ANNOTATIONS IN THE CLUSTER HERE
}
if (view.annotation!.isKindOfClass(ItemAnnotation) == true){
let annotation = view.annotation! as? ItemAnnotation
if let annotation = annotation, let item = annotation.item, d = delegate{
d.itemAnnotationPressed(item)
}
}
}
func mapView(mapView: MKMapView, didSelectAnnotationView view: MKAnnotationView) {
if (view.annotation!.isKindOfClass(FBAnnotationCluster) == true){
let annotation = view.annotation! as? FBAnnotationCluster
var itemListFromAnnotation = [Item]()
for annotation in (annotation?.annotations)! {
let itemAnnotation = annotation as? ItemAnnotation
itemListFromAnnotation.append((itemAnnotation?.item)!)
}
if let d = delegate{
d.itemClusterAnnotationPressed(itemListFromAnnotation)
}
}
if (view.annotation!.isKindOfClass(ItemAnnotation) == true){
mapView.deselectAnnotation(view.annotation, animated: false)
let annotation = view.annotation! as? ItemAnnotation
if let annotation = annotation, let item = annotation.item, d = delegate{
d.itemAnnotationPressed(item)
}
}
}