Swift MapKit - add (extra)attribut to annotation? - swift

I did this MapKit-Tutorial
Everything works fine, but how can I add an extra attribut to my pin?
This is my class Car:
import Foundation
import MapKit
class Car: NSObject, MKAnnotation {
let title: String
let subtitle: String
let thirdAttribut: String
let coordinate: CLLocationCoordinate2D
init(title: String, subtitle: String, thirdAttribut: String, coordinate: CLLocationCoordinate2D) {
self.title = title
self.subtitle = subtitle
self.thirdAttribut = thirdAttribut
self.coordinate = coordinate
super.init()
}
var getTitle: String {
return title
}
var getSubtitle: String {
return subtitle
}
var getThirdAttribut: String {
return thirdAttribut
}
I want click on an pin and work with this the "thirdAttribut".
My Main-Class:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
mapView.delegate = self
let car = Car(title: "Title", subtitle: "Subtitle", thirdAttribut: "ThirdAttribut", coordinate: CLLocationCoordinate2D(latitude: 50.906849, longitude: 7.524224) )
mapView.addAnnotation(car)
}
func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
//some config stuff
}
func mapView(mapView: MKMapView!, didSelectAnnotationView view: MKAnnotationView!) {
print("Here Comes the Click")
print(view.annotation.title)
print(view.annotation.subtitle)
//view.annotation.thirdAttribut doesn't existis
//How get I my "thirdAttribut"-Attribut?
}
The third-Attribut mustn't appear in my view. It just contains some data for logic-operations.
I hope u understand me, english isn't my mother tongue.
If u know other ways to code what I want then please tell me. :)
Thank u!

An annotation view contains an annotation property. To get that third attribute, you need to cast MKAnnotation to the class you created that adheres to the MKAnnotation protocol, Car.
if let carAnnotation = view.annotation as? Car{
print(carAnnotation.thirdAttrib);
}

Related

Adding annotations to MapKit in UIRepresentable

I'm working in SwiftUI and using MKMapView to show a map, annotations, and overlays on a view.
I was using the new Map() but the lack of overlay support has pulled me back to UIKit Representable.
When I was using Map it was easy to add annotations, but when using UIKitRepresentable I'm a bit confused on where to put the data, and how to make annotations from an array pulled from a network call.
Everything I've read has been either in Obj-C or adding a single annotation point. I'm trying to add (at present ~800) which is why I wanted to take advantage of the MKMapView in its reusability and clustering.
This is what I have at the moment:
struct UIMapView: UIViewRepresentable {
#EnvironmentObject var dataModel: DataModel
#EnvironmentObject var mapViewModel: MapViewModel
func makeCoordinator() -> Coordinator { Coordinator() }
func makeUIView(context: Context) -> MKMapView {
let view = mapViewModel.mapView
drawOverlayRing(view: view)
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: MKMapView, context: Context) {}
class Coordinator: NSObject, MKMapViewDelegate {
func mapView(_ map: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }
let identifier = "pinAnnotation"
var annotationView = map.dequeueReusableAnnotationView(
withIdentifier: identifier
) as? MKPinAnnotationView
if annotationView == nil {
annotationView = MKPinAnnotationView(
annotation: annotation,
reuseIdentifier: identifier
)
annotationView?.canShowCallout = true
} else {
annotationView?.annotation = annotation
}
return annotationView
}
}
func getAnnotations(view: MKMapView) {
for location in dataModel.locations {
let annotation = MKPointAnnotation()
annotation.title = location.title
annotation.coordinate = CLLocationCoordinate2D(
latitude: location.latitude,
longitude: location.longitude
)
view.addAnnotation(annotation)
}
}
}
I sometimes put my annotations in updateUIView, something like this:
func updateUIView() {
// remove the old ones
uiView.removeAnnotations(uiView.annotations)
uiView.addAnnotations(toMapAnnotations(locations: dataModel.locations))
}
func toMapAnnotations(locations: [CLLocationCoordinate2D]) -> [MapAnnotation] {
return locations.map { MapAnnotation(location: $0) }
}
final class MapAnnotation: NSObject, MKAnnotation {...}
The code will be provided later, since updateUIView is called multiple times.
I think it is best to avoid calling addAnnotations.

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.

Drag Custom Annotation View

I have my CoreData custom Annotation class:
extension Annotation: MKAnnotation {
public var coordinate: CLLocationCoordinate2D {
let cllCoordinate = CLLocationCoordinate2D(latitude: self.latitude, longitude: self.longitude)
return cllCoordinate
}
public var title: String? {
return self.objectId
}
class func keyPathsForValuesAffectingCoordinate() -> Set<String> {
return Set<String>([ #keyPath(latitude), #keyPath(longitude) ])
}
#nonobjc public class func fetchRequest() -> NSFetchRequest<Annotation> {
return NSFetchRequest<Annotation>(entityName: "Annotation")
}
#NSManaged public var latitude: Double
#NSManaged public var longitude: Double
#NSManaged public var dateTime: Date
#NSManaged public var type: String
#NSManaged public var objectId: String?
}
combined with a fetchedResultsController adding and deleting annotations works fine. But now a want to drag an annotation to another position. But only with setting isDraggable to true is not all. I can’t find newer descriptions how this could be integrated.
Here my viewFor method:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard annotation is Annotation else { return nil }
let identifier = "Annotation"
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView?.isDraggable = true
annotationView?.canShowCallout = true
} else {
annotationView!.annotation = annotation
}
let customAnno = annotation as! Annotation
let image = UIImage(named: customAnno.type)
annotationView!.image = image
return annotationView
}
What else do i need? I want to press on the annotation, stay pressed and go to another position, then release the finger and the annotation stays there.
When it drags, it needs to change the coordinate of the underlying annotation. It can’t do that if it’s read-only. So, if you want to make it draggable, you must give the coordinate property a setter:
public var coordinate: CLLocationCoordinate2D {
get {
CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
set {
latitude = newValue.latitude
longitude = newValue.longitude
}
}

class extension not being called - unable to configure annotations on map

I'm on Swift 5 and Xcode 11, currently trying to create a map with predefined annotations using custom images instead of the default markers. I've been playing around with Apple's sample code for AnnotatingMapWithCustomData and could tailor it to what I needed. But once I tried to copy the code into my own project, the annotations showed up as default markers instead of the custom annotationViews I configured.
import UIKit
import MapKit
class MapViewController: UIViewController {
#IBOutlet private weak var mapView: MKMapView!
private var allAnnotations: [MKAnnotation]?
private var displayedAnnotations: [MKAnnotation]? {
willSet {
if let currentAnnotations = displayedAnnotations {
mapView.removeAnnotations(currentAnnotations)
}
}
didSet {
if let newAnnotations = displayedAnnotations {
mapView.addAnnotations(newAnnotations)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
registerMapAnnotationViews()
// Create the array of annotations and the specific annotations for the points of interest.
allAnnotations = [ShrekAnnotation(), CoffeeAnnotation()]
// Dispaly all annotations on the map.
displayedAnnotations = allAnnotations
centerMapOnLondon()
}
/// - Tag: RegisterAnnotationViews
private func registerMapAnnotationViews() {
mapView.register(MKAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(ShrekAnnotation.self))
mapView.register(MKAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(CoffeeAnnotation.self))
}
private func centerMapOnLondon() {
let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
let center = CLLocationCoordinate2D(latitude: 51.507911, longitude: -0.132222)
mapView.setRegion(MKCoordinateRegion(center: center, span: span), animated: true)
}
}
extension MapViewController: MKMapViewDelegate {
/// - Tag: CreateAnnotationViews
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !annotation.isKind(of: MKUserLocation.self) else {
// Make a fast exit if the annotation is the `MKUserLocation`, as it's not an annotation view we wish to customize.
return nil
}
var annotationView: MKAnnotationView?
if let annotation = annotation as? ShrekAnnotation {
annotationView = setupShrekAnnotationView(for: annotation, on: mapView)
} else if let annotation = annotation as? CoffeeAnnotation {
annotationView = setupCoffeeAnnotationView(for: annotation, on: mapView)
}
return annotationView
}
/// - Tag: ConfigureAnnotationViews
private func setupShrekAnnotationView(for annotation: ShrekAnnotation, on mapView: MKMapView) -> MKAnnotationView {
let reuseIdentifier = NSStringFromClass(ShrekAnnotation.self)
let view = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier, for: annotation)
view.canShowCallout = true
// Provide the annotation view's image.
let image = #imageLiteral(resourceName: "map-shrek1")
view.image = image
return view
}
/// - Tag: ConfigureAnnotationViews
private func setupCoffeeAnnotationView(for annotation: CoffeeAnnotation, on mapView: MKMapView) -> MKAnnotationView {
let reuseIdentifier = NSStringFromClass(CoffeeAnnotation.self)
let view = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier, for: annotation)
view.canShowCallout = true
// Provide the annotation view's image.
let image = #imageLiteral(resourceName: "map-cof1")
view.image = image
return view
}
}
Apparently the class extension is not being called but I don't know how to fix this. The code that worked before I tried implementing it in my own project is identical. Could I have missed something I should've copied too or can you find the reason for the bug in the code?
Thanks for any help.
I think you are not setting the mapView delegate hence viewFor annotation is not being triggered and your custom annotation view is not appearing on map. Set
mapView.delegate = self
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
registerMapAnnotationViews()
// Create the array of annotations and the specific annotations for the points of interest.
allAnnotations = [ShrekAnnotation(), CoffeeAnnotation()]
// Dispaly all annotations on the map.
displayedAnnotations = allAnnotations
centerMapOnLondon()
}
Hope this helps

MKPointAnnotation and PFObject

Summary: Linking (NOT Query) Annotation with Parse from app
Completed:
What I'm trying to do is have the newly created annotation, become a PFObject as a result. The way I am attempting to do this is to get the coordinates based off of the MKPointAnnotation. Then turn that into a PFObject with coordinates. Then able to be Query later as a live update.
Problem
Still an issue is that with the code mentioned I'm not able to have the MKPointAnnotation which allows for me to add the title and UIImage, other than that all I would need tips on would be combining all of that together to finally be able to be queried from parse.
func handleLongPress(getstureRecognizer : UIGestureRecognizer) {
if getstureRecognizer.state != .Began { return }
let touchPoint = getstureRecognizer.locationInView(self.mapView)
let touchMapCoordinate = mapView.convertPoint(touchPoint, toCoordinateFromView: mapView)
createAnnotation(touchMapCoordinate)
}
private func createAnnotation(coordinate: CLLocationCoordinate2D) {
print("createAnnotation")
let myAnnotation = MyAnnotation(coordinate: coordinate)
mapView.addAnnotation(myAnnotation)
myAnnotation.saveInBackground()
}
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if !(annotation is MKPointAnnotation) {
return nil
}
let reuseId = "test"
var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId)
if pinView == nil {
pinView = MyAnnotation(annotation: annotation, reuseIdentifier: reuseId)
pinView!.image = UIImage(named:"Cloud9")
pinView!.canShowCallout = true
let rightButton: AnyObject! = UIButton(type: UIButtonType.ContactAdd)
pinView?.rightCalloutAccessoryView = rightButton as? UIView
It is not quite clear to me what you are asking about (for example with Linking (NOT Query) or become a PFObject as a result).
I have understood your problem as being the following:
A series of CLLocationCoordinate2D are created by the user
You want to easily add these coordinates to the MKMapView
You want to easily store these coordinates in the Parse backend
I think the best solution for you is to create a custom class which subclasses PFObject and implements MKAnnotation (I'm not quite sure what you mean by and that the only solution is to subclass the PFObject, which at its current state there is no help with that either.)
I've tried to put together a sample project with the following three files. It's nice and clean since you can have the following three lines of code to achieve the two goals outlined above:
let myAnnotation = MyAnnotation(coordinate: coordinate)
mapView.addAnnotation(myAnnotation)
myAnnotation.saveInBackground() // Handle this background save better
Let me know if this helps with your problem, otherwise please rephrase your question to be more clear.
UPDATE 1: OP wanted title, subtitle and image functionality.
The title and subtitle properties are not unique to MKPointAnnotation, but rather a part of the MKAnnotation protocol, which is used to create the MyAnnotation in my example. This means you simply need to add these two properties to MyAnnotation in order to get the functionality you want.
Concerning the image, then this is it not a unique feature for MKPinAnnotationView, but again for the more generic MKAnnotationView. In reality, the image property has not purpose in the MKPinAnnotationView. As described in the documentation for the image property:
You can use the `MKAnnotationView` class as is or subclass it to provide custom behavior as needed.
The `image` property of the class lets you set the appearance of the annotation view without subclassing directly.
When using the MKPinAnnotationView, then the view will always be a pin, which is why the image property has not purpose.
So in order to use this image property, then you'll need to make use of MKAnnotationView in the delegation method mapView:viewForAnnotation: from MKMapViewDelegate.
Remember to set the delegate property of the mapView (I've set it in the storyboard).
I've updated my answer in order to reflect the changes.
The code:
//
// ViewController.swift
// ParseAnnotationFun
//
// Created by Stefan Veis Pennerup on 28/01/16.
// Copyright © 2016 Kumuluzz. All rights reserved.
//
import UIKit
import MapKit
class ViewController: UIViewController, MKMapViewDelegate {
// MARK: - Storyboard outlets
#IBOutlet weak var mapView: MKMapView!
// MARK: - Gesture Recognizers
#IBAction func longPressed(sender: UILongPressGestureRecognizer) {
print("longPressed")
if sender.state != .Began { return }
let touchPoint = sender.locationInView(mapView)
let touchMapCoordinate = mapView.convertPoint(touchPoint, toCoordinateFromView: mapView)
createAnnotation(touchMapCoordinate)
}
// MARK: - Model
private func createAnnotation(coordinate: CLLocationCoordinate2D) {
print("createAnnotation")
let myAnnotation = MyAnnotation(coordinate: coordinate)
mapView.addAnnotation(myAnnotation)
myAnnotation.saveInBackground() // Handle this background save better
}
// MARK: - MKMapViewDelegate
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
let id = "someIdentifier"
var view = mapView.dequeueReusableAnnotationViewWithIdentifier(id)
if view == nil {
view = MKAnnotationView(annotation: annotation, reuseIdentifier: id)
view?.canShowCallout = true
view?.image = UIImage(named: "oranges")
}
view?.annotation = annotation
return view
}
}
//
// MyAnnotation.swift
// ParseAnnotationFun
//
// Created by Stefan Veis Pennerup on 28/01/16.
// Copyright © 2016 Kumuluzz. All rights reserved.
//
import Foundation
import Parse
import MapKit
class MyAnnotation: PFObject, PFSubclassing, MKAnnotation {
// MARK: - Properties
#NSManaged var location: PFGeoPoint
// MARK: - Initializers
init(coordinate: CLLocationCoordinate2D) {
super.init()
location = PFGeoPoint(latitude: coordinate.latitude, longitude: coordinate.longitude)
}
override class func initialize() {
struct Static {
static var onceToken : dispatch_once_t = 0;
}
dispatch_once(&Static.onceToken) {
self.registerSubclass()
}
}
// MARK: - PFSubclassing protocol
static func parseClassName() -> String {
return "AnnotationPins"
}
// MARK: - MKAnnotation protocol
var coordinate: CLLocationCoordinate2D {
return CLLocationCoordinate2DMake(location.latitude, location.longitude)
}
var title: String? = "Awesome title"
var subtitle: String? = "Random subtitle"
}
//
// AppDelegate.swift
// ParseAnnotationFun
//
// Created by Stefan Veis Pennerup on 28/01/16.
// Copyright © 2016 Kumuluzz. All rights reserved.
//
import UIKit
import Parse
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
MyAnnotation.initialize()
Parse.setApplicationId("xxx",
clientKey: "xxx")
return true
}
}