I have a custom MkAnnotation that has a title, subtitle, and extra information associated with it for use on an map view. I am passing this information to another UIViewController. However there seems to be some issue with my coordinate that I have allocated for the pin. The error is highlighted in the setting information of pin below on the line of code "var bridge17pin = MyAnnotation()".
override func viewDidLoad() {
super.viewDidLoad()
//for users location
myLocMgr.desiredAccuracy = kCLLocationAccuracyBest
myLocMgr.requestWhenInUseAuthorization()
myLocMgr.startUpdatingLocation()
myLocMgr.delegate = self
mapView.delegate = self
//coordinate for pin
var bridge17 = CLLocationCoordinate2DMake(53.346061, -6.227379)
//custom MKAnnotation
class MyAnnotation: NSObject, MKAnnotation {
#objc var coordinate: CLLocationCoordinate2D
var EXTRA_INFORMATION: String?
var title: String?
init(coordinate: CLLocationCoordinate2D) {
self.coordinate = coordinate
}
}
//setting information of pin
var bridge17pin = MyAnnotation()
bridge17pin.coordinate = bridge17
bridge17pin.title = "The bridge"
bridge17pin.subtitle = "hello this is the bridge"
bridge17pin.EXTRA_INFORMATION = "this was built in 2010"
mapView.addAnnotation(bridge17pin)
}
Because you have to send the coordinate while you are instantiating. So in the code replace
var bridge17pin = MyAnnotation()
with
var bridge17pin = MyAnnotation(coordinate: bridge17)
And for that subTitle see this link
Related
I am creating an application that receives the localization information from an Apple Watch and with the coordinates that I receive, I want to create a MapKit annotation on a, I also show the location of the Phone.
But when I try to create the array to hold the information for the annotation the error
Cannot use instance member 'connectivity' within property initializer; property initializers run before 'self' is available
The class that is giving me the error is the following:
MapView.swift
import MapKit
import SwiftUI
// Struct to display devices location
struct Place: Identifiable {
let id = UUID()
let name: String?
let coordinate: CLLocationCoordinate2D?
init(name: String? = nil,
coordinate: CLLocationCoordinate2D? = nil) {
self.name = name
self.coordinate = coordinate
}
}
struct MapView: View {
// Loads the ViewModels for the map
#StateObject private var viewModel = MapViewModel()
// Loads location of apple watch
#StateObject private var connectivity = Connectivity()
// Start tracking
#State var toggleTracking = true
#State var showTemporaryPopUp = true
// contains the location coordinates of device, to draw where is located on the map
var annotations = [
Place(name: "Apple Watch", coordinate: CLLocationCoordinate2D(latitude: connectivity.receivedLat, longitude: connectivity.receivedLong))
]
var body: some View {
ZStack(alignment: .top) {
// Displays Map
Map(coordinateRegion: $viewModel.region, showsUserLocation: true)
.ignoresSafeArea()
.accentColor(Color(.systemPink))
}
The part that is giving me the error is this one:
// contains the location coordinates of device, to draw where is located on the map
var annotations = [
Place(name: "Apple Watch", coordinate: CLLocationCoordinate2D(latitude: connectivity.receivedLat, longitude: connectivity.receivedLong))
]
And here is the class that contains the latitude and longitude, and the source of the error.
Connectivity.swift
import Foundation
import WatchConnectivity
class Connectivity: NSObject, ObservableObject, WCSessionDelegate {
#Published var receivedText = ""
#Published var receivedLat: Double = 0.0
#Published var receivedLong: Double = 0.0
override init() {
super.init()
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate()
}
}
// This function receives the information sent from the watch,
//and then sets the published vars with the information
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
DispatchQueue.main.async {
if let lat = message["latitude"] as? Double, let lon = message["longitud"] as? Double {
self.receivedText = "\(lat),\(lon)"
self.receivedLat = lat
self.receivedLong = lon
replyHandler(["Response": "Be excellent to each other"])
}
}
}
}
The first thing that I tried is using the 'lazy' var, like this:
// contains the location coordinates of device, to draw where is located on the map
lazy var annotations = [
Place(name: "Apple Watch", coordinate: CLLocationCoordinate2D(latitude: connectivity.receivedLat, longitude: connectivity.receivedLong))
]
But then the next error appears when I try to get the coordinates:
Cannot use mutating getter on immutable value: 'self' is immutable
So this solution is out of the question.
And the other common solution is to use the init, the way I am doing it is the following way, on my Connectivity.swift file:
override init() {
super.init()
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate()
}
self.receivedLat = 0.0
self.receivedLong = 0.0
}
But the error still exist, and I don't know some other way to change the init.
Is there other way to do it?
Sources:
Hudson, P. (2020). Hacking with watchOS
Make annotations a computed property -- that way, it has access to connectivity and it will get re-computed with the View re-renders because a #Published property of Connectivity has changed:
var annotations : [Place] {
[
Place(name: "Apple Watch", coordinate: CLLocationCoordinate2D(latitude: connectivity.receivedLat, longitude: connectivity.receivedLong))
]
}
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.
So I'm creating my annotations in a for loop as follows in the update map view function:
for item in array {
let latitude = item.latitude
let longitude = item.longitude
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
let annotation = //make an annotation that uses a different image in Assets depending on a variable called "imageID"
mapView.addAnnotation(annotation)
}
I tried using reuseIdentifiers but wasn't even getting close to figuring this out. I also tried using the imageFor function and making a custom annotation class (to detect what the image passed in was and change the view accordingly) but it didn't work:
class CustomAnnotation: NSObject, MGLAnnotation {
var coordinate: CLLocationCoordinate2D
var title: String?
var subtitle: String?
var image: UIImage?
init(coordinate: CLLocationCoordinate2D, title: String?, subtitle: String?) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
}
}
func mapView( _ mapView: MGLMapView, imageFor annotation: MGLAnnotation ) -> MGLAnnotationImage? {
let annotationImage: MGLAnnotationImage
let annotationImageToilet = mapView.dequeueReusableAnnotationImage(withIdentifier: "custom1")
let annotationImageDefault = mapView.dequeueReusableAnnotationImage(withIdentifier: "defaultImage")
switch annotation.image { //tried to sort according to the image value of the custom class
case "custom1":
var featureAnnotation = FeatureAnnotation(coordinate: annotation.coordinate, title: "Title", subtitle: "subtitle")
featureAnnotation.image = UIImage(named: "custom1")!
annotationImage = annotationImageToilet ?? featureAnnotation //couldn't get this line working properly
default:
var featureAnnotation = FeatureAnnotation(coordinate: annotation.coordinate, title: "Title", subtitle: "subtitle")
featureAnnotation.image = UIImage(named: "defaultImage")!
annotationImage = annotationImageDefault ?? featureAnnotation //couldn't get this line working properly
}
return annotationImage
}
Essentially, I just want the image of the custom annotations to change depending on what feature.imageID is, any help much appreciated!
I do not find how link my let = pin
Help me please
My first code:
#IBOutlet weak var Point1S: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
let location = CLLocationCoordinate2DMake(43.429837, 5.434837)
Point1S.setRegion(MKCoordinateRegionMakeWithDistance(location, 500, 500), animated: true)
let pin = here
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
Second code:
import MapKit
class MKPinAnnotationView : NSObject , MKAnnotation {
var title: String?
var subtitle: String?
var coordinate: CLLocationCoordinate2D
init(title : String , subtitle : String , coordinate : CLLocationCoordinate2D) {
self.title = title
self.subtitle = subtitle
self.coordinate = coordinate
}
}
I do not know how to put on my first code in the let pin = the second code
thanks
let pin = MKPinAnnotationView(title: "title", subtitle: "subtitle", coordinate: CLLocationCoordinate2DMake(27.33, 85.03))
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);
}