MapKit Display Annotation Clusters and Along With Non-Clustered Annotations - swift

I am new to iOS development and currently using the FBAnnotationClusteringSwift to cluster makers. My app needs to have two annotations which will not be clustered, as they indicate the source and destination addresses. The remaining annotations must be clustered as they represent stations.
What I want to achieve is something like the image bellow, where "A" is my source address, and the clusters represent stations:
However what is happening is, as the clusters are created, the Annotation that represents a non-clustered annotation (the source address) disappears as following:
If the library is mixing all annotations together, that is pretty bad, there should be a way to separate those that I've added through clusteredAnnotationsWithinMapRect (stations) and those markers that were already added before (addresses). Here is the relevant part of the current code:
var markerClusterer: FBClusteringManager?
ViewController ... {
override func viewDidLoad() {
super.viewDidLoad()
...
self.markerClusterer = FBClusteringManager()
self.markerClusterer!.delegate = self
}
func cellSizeFactorForCoordinator(coordinator:FBClusteringManager) -> CGFloat{
return 1.0
}
func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// Here is just like the example code from FBAnnotationClusterSwift
var stations: [MKAnnotation] = fetchStations(...)
if (stations.count > 0) {
NSOperationQueue().addOperationWithBlock({
let scale:Double = Double(self.mapView.bounds.size.width) / self.mapView.visibleMapRect.size.width
self.markerClusterer!.addAnnotations(stations)
var annotationArray = stations
// Needed to do that otherwise the clusters never "explode" into pins
if (scale <= 0.06) {
annotationArray = self.markerClusterer!.clusteredAnnotationsWithinMapRect(self.mapView.visibleMapRect, withZoomScale:scale)
}
self.markerClusterer!.displayAnnotations(annotationArray, onMapView: self.mapView)
})
}
}
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
var reuseId = ""
if (annotation.isKindOfClass(FBAnnotationCluster)) {
reuseId = "Cluster"
var clusterView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId)
if clusterView == nil {
clusterView = FBAnnotationClusterView(annotation: annotation, reuseIdentifier: reuseId, options: nil)
} else {
clusterView!.annotation = annotation
}
return clusterView
} else if (annotation.isKindOfClass(AddressPointAnnotation)) {
reuseId = "AddressPin"
var addressPinView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId)
if addressPinView == nil {
addressPinView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
addressPinView!.canShowCallout = true
}
else {
addressPinView!.annotation = annotation
}
let addressPin = annotation as! AddressPointAnnotation
addressPinView!.image = UIImage(named: addressPin.icon)
return addressPinView
} else if (annotation is Station) {
reuseId = "StationPin"
var stationPinView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId)
if stationPinView == nil {
stationPinView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
}
else {
stationPinView!.annotation = annotation
}
let stationPin = annotation as! Station
stationPinView!.image = UIImage(named: "station")
return stationPinView
} else {
return nil
}
}
}
// Annotation for the stations
class Station: NSObject, MKAnnotation {
var id: Int = 0
var availableBikes: Int = 0
var availableBikeStands: Int = 0
var coordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 39.208407, longitude: -76.799555)
}
// Annotations for addresses
class AddressPointAnnotation: MKPointAnnotation {
var icon: String!
}
class Address: NSObject {
var marker: AddressPointAnnotation?
func addMarker(coordinate: CLLocationCoordinate2D) {
marker = AddressPointAnnotation()
marker!.coordinate = coordinate
// ViewController is passed to the Address, so it can add itself to the map
self.controller.mapView.addAnnotation(marker!)
if (self.direction == SOURCE) {
marker!.title = "Starting address"
marker!.icon = "from"
} else {
marker!.title = "Destination address"
marker!.icon = "to"
}
self.controller.mapView.addAnnotation(marker!)
}
}
Any help, idea, or comment is more than welcome. Thanks.

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.

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.

Swift Custom Annotation does not get select

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.

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

MKPointAnnotations touch event in swift

I would like to know if anyone can tell me how I can touch a pin on the map in the form of MKPointAnnotations .
I would like to click the pin on the map and go to another view by bringing back the variables of the pin that I have preset .
Can anyone explain me this thing in Swift ?
thanks
Edit with code:
class ViewController: UIViewController, MKMapViewDelegate {
#IBOutlet weak var mappa: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
var location : CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 44.648590, longitude: 10.918794)
let pinAnnotation = PinAnnotation()
pinAnnotation.setCoordinate(location)
self.mappa.addAnnotation(pinAnnotation)
}
class PinAnnotation : NSObject, MKAnnotation {
private var coord: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, longitude: 0)
var coordinate: CLLocationCoordinate2D {
get {
return coord
}
}
var title: String = "test"
var subtitle: String = "test"
func setCoordinate(newCoordinate: CLLocationCoordinate2D) {
self.coord = newCoordinate
}
}
func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
if annotation is PinAnnotation {
let pinAnnotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "myPin")
pinAnnotationView.pinColor = .Purple
pinAnnotationView.draggable = true
pinAnnotationView.canShowCallout = true
pinAnnotationView.animatesDrop = true
let deleteButton = UIButton.buttonWithType(UIButtonType.Custom) as UIButton
deleteButton.frame.size.width = 44
deleteButton.frame.size.height = 44
deleteButton.backgroundColor = UIColor.redColor()
deleteButton.setImage(UIImage(named: "trash"), forState: .Normal)
pinAnnotationView.leftCalloutAccessoryView = deleteButton
return pinAnnotationView
}
return nil
}
func mapView(mapView: MKMapView!, annotationView view: MKAnnotationView!, calloutAccessoryControlTapped control: UIControl!) {
if let annotation = view.annotation as? PinAnnotation {
self.mapView.removeAnnotation(annotation)
}
}
}
Several steps are necessary, here are some code snippets to get you started.
First you need a custom class for your pin annotation which holds the data you want to work with.
import MapKit
import Foundation
import UIKit
class PinAnnotation : NSObject, MKAnnotation {
private var coord: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, longitude: 0)
private var _title: String = String("")
private var _subtitle: String = String("")
var coordinate: CLLocationCoordinate2D {
get {
return coord
}
}
func setCoordinate(newCoordinate: CLLocationCoordinate2D) {
self.coord = newCoordinate
}
var title: String? {
get {
return _title
}
set (value) {
self._title = value!
}
}
var subtitle: String? {
get {
return _subtitle
}
set (value) {
self._subtitle = value!
}
}
}
Then you need a custom class for your MKMapView which conforms to the MKMapViewDelegate protocol. Implement the method viewForAnnotation there:
import MapKit
import CLLocation
import Foundation
import UIKit
class MapViewController: UIViewController, MKMapViewDelegate {
...
func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
if annotation is PinAnnotation {
let pinAnnotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "myPin")
pinAnnotationView.pinColor = .Purple
pinAnnotationView.draggable = true
pinAnnotationView.canShowCallout = true
pinAnnotationView.animatesDrop = true
let deleteButton = UIButton.buttonWithType(UIButtonType.Custom) as UIButton
deleteButton.frame.size.width = 44
deleteButton.frame.size.height = 44
deleteButton.backgroundColor = UIColor.redColor()
deleteButton.setImage(UIImage(named: "trash"), forState: .Normal)
pinAnnotationView.leftCalloutAccessoryView = deleteButton
return pinAnnotationView
}
return nil
}
func mapView(mapView: MKMapView!, annotationView view: MKAnnotationView!, calloutAccessoryControlTapped control: UIControl!) {
if let annotation = view.annotation as? PinAnnotation {
mapView.removeAnnotation(annotation)
}
}
That gives you something like this:
To add a new annotation to your map use this somewhere in your code:
let pinAnnotation = PinAnnotation()
pinAnnotation.setCoordinate(location)
mapView.addAnnotation(pinAnnotation)
Greate work !!! BUT..
I just copy past this and I had to add a few changes. I'll share with you guys those changes.
import MapKit
import Foundation
import UIKit
class PinAnnotation : NSObject, MKAnnotation {
private var coord: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, longitude: 0)
private var _title: String = String("")
private var _subtitle: String = String("")
var title: String? {
get {
return _title
}
set (value) {
self._title = value!
}
}
var subtitle: String? {
get {
return _subtitle
}
set (value) {
self._subtitle = value!
}
}
var coordinate: CLLocationCoordinate2D {
get {
return coord
}
}
func setCoordinate(newCoordinate: CLLocationCoordinate2D) {
self.coord = newCoordinate
}
}
I hope it will help :D