My problem is similar to this one
Displaying custom multiple pins shows wrong pins for locations
but slightly different and I'm working in Swift. Even after converting, I couldn't get the solution to my problem.
I have annotations with a different image for each. The images are numbered pointers. The details of each annotation comes from a plist. The titles are such as "1 car park", "2 mill" etc. I take the part before the space to get the number which corresponds to a png image.
On loading, all annotations have the correct image. If I pan off the area and come back the images can swap with each other. It seems to be a problem of reusing, but I can't see the solution. getMapAnnotations() is called in viewdidload.
func getMapAnnotations() -> [Stands] {
var annotations:Array = [Stands]()
print("ANNOTATIONS")
//load plist file
var stands: NSArray?
if let path = Bundle.main.path(forResource: "stands", ofType: "plist") {
stands = NSArray(contentsOfFile: path)
}
//iterate and create annotations
if let items = stands {
for item in items {
let lat = (item as AnyObject).value(forKey: "lat") as! Double
let long = (item as AnyObject).value(forKey: "long")as! Double
let annotation = Stands(latitude: lat, longitude: long)
let tit = (item as AnyObject).value(forKey: "title") as! String
let numb = (item as AnyObject).value(forKey: "no") as! Int
annotation.title = "\(numb) \(tit)"
annotation.no = numb
annotations.append(annotation)
}
}
return annotations
}
then the annotation view
func mapView(_ mapView: MKMapView!, viewFor annotation: MKAnnotation!) -> MKAnnotationView! {
let identifier = "Stand"
var imageI: String?
var imageAn: String?
imageI = annotation.title!
if annotation.isKind(of: Stands.self) {
print("ANNOTATION \(imageI!)")
if let range = imageI?.components(separatedBy: " ") {
var imageII = range[0]
imageAn = "\(imageII).png"
print (imageAn!)
}
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier)
//
annotationView?.image = UIImage(named: imageAn! )
annotationView?.canShowCallout = true
annotationView?.layer.zPosition = -1
let btn = UIButton(type: .detailDisclosure)
annotationView?.rightCalloutAccessoryView = btn
} else {
annotationView!.annotation = annotation
}
return annotationView
}
return nil
}
I seem to have found the solution , more by trial and error than knowing what I'm doing.
I added
annotationView?.image = UIImage(named: imageAn! )
to make
}else{
annotationView?.image = UIImage(named: imageAn! )
annotationView.annotation = annotation
}
if I understand correctly , annotation is the information of the coordinates, title and subtitle with no information about display.
AnnotationView sets up how the annotation will be displayed. With a pin or custom image.
If AnnotationView is nill the annotation views are created correctly. If the annotations go out of view and come back, AnnotationView is not nil so the system takes a view at random to display it.
Adding
annotationView?.image = UIImage(named: imageAn! )
forces the correct image to be displayed.
I'm sure someone will correct me if this schema isn't correct.
Related
Im creating an app that shows different locations. Currently when a button is pressed the car locations pop up on the map, however I want to then hide those shown markers if that same button is pressed again.
This is the function that takes a snapshot of my database from firebase, it then inserts the GMSMarker into the location.
func showCarIcon() {
ref = Database.database().reference()
ref.child("location").observe(.childAdded) { (snapshot:DataSnapshot) in
if let dict = snapshot.value as? [String:AnyObject] {
if dict["Activity"] as! String == "Car" {
let longitude = dict["Longitude"] as! String
let lattitude = dict["Lattitude"] as! String
let title = dict["Title"] as! String
self.carIconArray.insert(coordinate(carLat: lattitude, carLng: longitude), at: 0)
let n = self.carIconArray.count
let heightWidth = self.mapView.frame.height
for marker in 1...n {
let carMarker = GMSMarker()
let carIconView = UIImage(named: "carPin")
let image = carIconView
let location = CLLocationCoordinate2D(latitude: Double(lattitude)!, longitude: Double(longitude)!)
carMarker.position = location
carMarker.icon = image
carMarker.title = title
carMarker.icon = self.image(image!, scaledToSize: CGSize(width: heightWidth/6, height: heightWidth/6))
func displayIt() {
if self.carNumber == "1" {
carMarker.map = self.mapView
} else {
carMarker.map = nil
}
}
displayIt()
}
}
}
}
}
So this is the action function for when button is pressed.
var carNumber = String()
#IBAction func showCar(_ sender: Any) {
if motorisedVehicleButtonActive {
motorisedVehicleButton.setImage(UIImage(named: "carO"), for: .normal)
carNumber = "1"
} else {
motorisedVehicleButton.setImage(UIImage(named: "car"), for: .normal)
carNumber = "0"
}
print(carNumber)
motorisedVehicleButtonActive = !motorisedVehicleButtonActive
showCarIcon()
}
Let me explain what is issue with your code.
You are creating new marker every time when button press. So, new marker have different object id than older.
When you try to remove it, it will not works just because of it's different marker than you placed on map.
So you need to store marker in array and on remove time, get icon from array and remove it from map.
First you need to create array of GMSMarker, because you have to store every marker which is placed on map.
So, write following line of code at top of your class.
var arrCarMarkers = [GMSMarker]()
Then after, store every marker in this array which are you placing on map.
So, update your code as follow:
func displayIt() {
if self.carNumber == "1" {
carMarker.map = self.mapView
arrCarMarkers.append(carMarker) // Here is store marker in array
} else {
carMarker.map = nil
}
}
Now, you have all marker which are placed on map. So when you want to remove these markers just update your code as follow:
#IBAction func showCar(_ sender: Any) {
if motorisedVehicleButtonActive {
motorisedVehicleButton.setImage(UIImage(named: "carO"), for: .normal)
carNumber = "1"
showCarIcon()
} else {
motorisedVehicleButton.setImage(UIImage(named: "car"), for: .normal)
carNumber = "0"
self.arrCarMarkers.forEach { $0.map = nil }
}
print(carNumber)
motorisedVehicleButtonActive = !motorisedVehicleButtonActive
}
Above code will remove all markers from map.
I hope this will works for you.
I am attempting to show multiple objects on my map annotations from Care Data. Currently, I can get title and subtitle to show info with this function
func getData() -> [MKAnnotation]? {
do {
storedLocations = try context.fetch(Fish.fetchRequest())
var annotations = [MKAnnotation]()
for storedLocation in storedLocations {
let newAnnotation = MyAnnotation(coordinate: CLLocationCoordinate2D(latitude: storedLocation.latitude, longitude: storedLocation.longitude))
newAnnotation.coordinate.latitude = storedLocation.latitude
newAnnotation.coordinate.longitude = storedLocation.longitude
newAnnotation.title = storedLocation.species
newAnnotation.subtitle = storedLocation.length
newAnnotation.newTime = storedLocation.time
newAnnotation.newBait = storedLocation.bait
newAnnotation.newNotes = storedLocation.notes
newAnnotation.newWaterTempDepth = storedLocation.water
newAnnotation.newWeatherCond = storedLocation.weather
// print(storedLocation.length)
let newLength = storedLocation.length
let newTime = newAnnotation.newTime
// let newBait = storedLocation.bait
// let newNotes = storedLocation.notes
// let newWaterTempDepth = storedLocation.water
// let newWeatherCond = weatherCond
myLength = newLength!
myTime = newTime!
//
annotations.append(newAnnotation)
}
return annotations
}
catch {
print("Fetching Failed")
}
return nil
}
I am trying to show all of the storedLocation data on the annotation.
By using this extension I found here I am able to add multiple lines to the annotation.
However it is not from the info stored in core data. This is how I set up the func for the extension
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !(annotation is MKUserLocation) else {
return nil
}
let reuseIdentifier = "pin"
var annotationView = map.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
annotationView?.canShowCallout = true
annotationView?.loadCustomLines(customLines: [myLength, myTime])
} else {
annotationView?.annotation = annotation
}
annotationView?.image = UIImage(named: "fish")
return annotationView
Here is the extension.
extension MKAnnotationView {
func loadCustomLines(customLines: [String]) {
let stackView = self.stackView()
for line in customLines {
let label = UILabel()
label.text = line
stackView.addArrangedSubview(label)
}
self.detailCalloutAccessoryView = stackView
}
private func stackView() -> UIStackView {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.alignment = .fill
return stackView
}
}
here is my annotation
import UIKit
import MapKit
class MyAnnotation: NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D
var title: String?
var subtitle: String?
var newLength: String?
var newTime: String?
var newBait: String?
var newNotes: String?
var newWaterTempDepth: String?
var newWeatherCond: String?
var detailCalloutAccessoryView: UIStackView?
init(coordinate: CLLocationCoordinate2D){
self.coordinate = coordinate
}
}
When I run I get
this
I am very new to programming and would greatly appreciate any help, and also an explanation of what I am doing wrong Thank you in advance
'!' means Force Unwrapping
myLength = newLength! <- CRASH
As 'newLength' is nil here, you will get a crash on the line where you force unwrap it.
e.g. You can fix the error
myLength = newLength ?? 0
myTime = newTime ?? 0
See also What does "fatal error: unexpectedly found nil while unwrapping an Optional value" mean?
I'm trying to change the map annotation to a custom image that i made in a static cell. The map's annotation works but then when i try to put my image in the map zooms to the location but doesn't display the image.
import UIKit
import MapKit
import CoreLocation
class ContactsTableViewController: UITableViewController, MKMapViewDelegate, CLLocationManagerDelegate {
}
override func viewDidLoad() {
super.viewDidLoad()
self.addressMap.delegate = self
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath) as! customCell
if let _ = currentAnnotation {
addressMap.removeAnnotation(currentAnnotation!)
}
let contact = contacts.contactWithIndex((indexPath as NSIndexPath).row)
print(contact)
cell.nameLabel.text = contact.name
cell.addressLabel.text = contact.address
let country = "USA"
let city = "Cicero"
let street = contact.address
// Create Address String
let address = "\(country), \(city), \(street)"
// Geocode Address String
geocoder.geocodeAddressString(address) { (placemarks, error) in
// Process Response
self.processResponse(withPlacemarks: placemarks, error: error)
var location: CLLocation?
if let placemarks = placemarks, placemarks.count > 0 {
location = placemarks.first?.location
}
if let location = location {
let coordinate = location.coordinate
print("Custom cell: \(coordinate.latitude), \(coordinate.longitude)")
//Drops pin to current MKPointAnnotationis logged in
self.currentAnnotation = MKPointAnnotation()
self.currentAnnotation?.coordinate = CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: coordinate.longitude)
let image1 = CustomPointAnnotation()
image1.imageName = "mapPin.png"
self.addressMap.addAnnotation(image1)
//Pins the location on the mapview
//cell.addressMap.addAnnotation(self.currentAnnotation!)
cell.addressMap.addAnnotation(image1)
//Auto zoom into user location
let span = MKCoordinateSpanMake(0.02, 0.02)
let region = MKCoordinateRegion(center: (self.currentAnnotation?.coordinate)!, span: span)
cell.addressMap.setRegion(region, animated: true)
} else {
print("No Matching Location Found")
}
}
}
func addressMap(_ addressMap: MKMapView, viewFor annotation: MKAnnotation!) -> MKAnnotationView? {
if !(annotation is CustomPointAnnotation) {
return nil
}
let reuseId = "test"
var anView = addressMap.dequeueReusableAnnotationView(withIdentifier: reuseId)
if anView == nil {
anView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
anView?.canShowCallout = true
} else {
anView?.annotation = annotation
}
//Set annotation-specific properties **AFTER**
//the view is dequeued or created...
let cpa = annotation as! CustomPointAnnotation
anView?.image = UIImage(named:cpa.imageName)
return anView
}
Here's the class for the annotation
import UIKit
import MapKit
class CustomPointAnnotation: MKPointAnnotation {
var imageName: String!
}
If anyone can tell me what I'm doing wrong that's causing my image to not display as the annotation that would be awesome.
A couple of issues:
You don't appear to ever set the coordinate of the CustomPointAnnotation.
The signature for the mapView(_:viewFor:) is incorrect. If you add a breakpoint in your method, you'll see it is not called. Your map outlet may be called addressMap, but the delegate method name is mapView(_:viewFor:), regardless of the name of your MKMapView outlet reference.
Firstly please don't mark this as a duplicate. I have been through all the other questions and answers and already improved the code I first had but I still can't figure out the last part. I have my final year project due tomorrow and would love to get this feature working. Thank you!!
I have a custom callout from an annotation and when I click a button on it I want to get directions from the users location to that annotation.
let buttonDirections = UIButton(frame: calloutView.directions.frame)
buttonDirections.addTarget(self, action: #selector(FindParkingVC.getDirections(sender:)), for: .touchUpInside)
calloutView.addSubview(buttonDirections)
func getDirections(sender: UIButton) {
//error on this line saying UIbutton has no member view
//obviously the sender is wrong but don't know how to fix it
if let anno = sender.view.annotation as? SpaceAnnotation
{
var place: MKPlacemark!
if #available(iOS 10.0, *) {
place = MKPlacemark(coordinate: anno.coordinate)
} else {
place = MKPlacemark(coordinate: anno.coordinate, addressDictionary: nil)
}
let destination = MKMapItem(placemark: place)
destination.name = "Selected Parking Space"
let regionDistance: CLLocationDistance = 1000
let regionSpan = MKCoordinateRegionMakeWithDistance(anno.coordinate, regionDistance, regionDistance)
let options = [MKLaunchOptionsMapCenterKey: NSValue(mkCoordinate: regionSpan.center), MKLaunchOptionsMapSpanKey: NSValue(mkCoordinateSpan: regionSpan.span), MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving] as [String : Any]
MKMapItem.openMaps(with: [destination], launchOptions: options)
}
}
Now I previously had this all working when I was using the standard callout. The function worked perfectly like this. I'm not using the detailcalloutbutton now so i can't use this function.
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
if let anno = view.annotation as? SpaceAnnotation {
var place: MKPlacemark!
if #available(iOS 10.0, *) {
place = MKPlacemark(coordinate: anno.coordinate)
} else {
place = MKPlacemark(coordinate: anno.coordinate, addressDictionary: nil)
}
let destination = MKMapItem(placemark: place)
destination.name = "Selected Parking Space"
let regionDistance: CLLocationDistance = 1000
let regionSpan = MKCoordinateRegionMakeWithDistance(anno.coordinate, regionDistance, regionDistance)
let options = [MKLaunchOptionsMapCenterKey: NSValue(mkCoordinate: regionSpan.center), MKLaunchOptionsMapSpanKey: NSValue(mkCoordinateSpan: regionSpan.span), MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving] as [String : Any]
MKMapItem.openMaps(with: [destination], launchOptions: options)
}
}
EDIT
This is how I am calling the CustomCallout View
func mapView(_ mapView: MKMapView,
didSelect view: MKAnnotationView)
{
if view.annotation is MKUserLocation
{
// Don't proceed with custom callout
return
}
let SpaceAnnotation = view.annotation as! SpaceAnnotation
let views = Bundle.main.loadNibNamed("CustomCalloutView", owner: nil, options: nil)
let calloutView = views?[0] as! CustomCalloutView
if SpaceAnnotation.temp3 != nil {
calloutView.duration.text = "Available for " + SpaceAnnotation.temp3
}
if SpaceAnnotation.temp != nil {
calloutView.price.text = "€" + SpaceAnnotation.temp
}
if SpaceAnnotation.temp2 != nil {
calloutView.description_.text = "Description: " + SpaceAnnotation.temp2
}
let buttonDirections = UIButton(frame: calloutView.directions.frame)
buttonDirections.addTarget(self, action: #selector(FindParkingVC.getDirections(sender:)), for: .touchUpInside)
calloutView.addSubview(buttonDirections)
calloutView.center = CGPoint(x: view.bounds.size.width / 2, y: -calloutView.bounds.size.height*0.52)
view.addSubview(calloutView)
mapView.setCenter((view.annotation?.coordinate)!, animated: true)
}
Also maybe worth noting SpaceAnnotation is a class which contents the variables temp3 etc. so in the init it would have more of the following
temp3 = snapshotValue["duration"] as? String
UIButton added to CustomCalloutView then its added to MKAnnotationView. Therefore, the MKAnnotationView is the superview of the superview (two steps up):
if let anno = (sender.superview?.superview as! MKAnnotationView).annotation as? SpaceAnnotation {
}
How can I set my map MKAnnotation CalloutAccessoryView to show the selected image mapped out from my realm database (with icons and buttons on either side, as the left and right calloutAccessory?). I also need it to recognize the class/member for other functions on my MapViewController here? The mapping of the data and left and right callout accessories are working fine, but I can't figure out how to add the selected image, so it show up as a large poster-style image on the callout (with the small icons & buttons on either side).
The image is SpecimenAnnotation.objectPoster
The SpecimenAnnotation (which is working fine to map the data) and 'objectPoster' value is the selected (and mapped) Annotation from the class Specimen, included in my file Specimen.swift, like this;
class Specimen: Object{
dynamic var name = ""
dynamic var objectPoster = ""
dynamic var latitude = 0.0
dynamic var longitude = 0.0
dynamic var created = NSDate()
dynamic var category: Category!
}
In my MapViewController, on the line 'annotationView.detailCalloutAccessoryView = UIImageView(image: SpecimenAnnotation.objectPoster)' I am getting a red alert error "instance member 'objectPoster' cannot be used on type 'Specimen Annotation'
I am a beginner. Would greatly appreciate your suggestions. Any ideas?
Here are the code blocks regarding this data and error:
// Create annotations for each one
for specimen in specimens { // 3
let coord = CLLocationCoordinate2D(latitude: specimen.latitude, longitude: specimen.longitude);
let specimenAnnotation = SpecimenAnnotation(coordinate: coord, poster: specimen.objectPoster, subtitle: specimen.category.name, specimen: specimen)
mapView.addAnnotation(specimenAnnotation) // 4
}
}
}
//MARK: - MKMapview Delegate
extension MapViewController: MKMapViewDelegate {
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
guard let subtitle = annotation.subtitle! else { return nil }
if (annotation is SpecimenAnnotation) {
if let annotationView = mapView.dequeueReusableAnnotationViewWithIdentifier(subtitle) {
return annotationView
} else {
let currentAnnotation = annotation as! SpecimenAnnotation
let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: subtitle)
annotationView.image = UIImage(named:"IconStudent")
annotationView.enabled = true
annotationView.canShowCallout = true
// ERROR message here>
annotationView.detailCalloutAccessoryView = UIImageView(image: SpecimenAnnotation.objectPoster)
// right accessory view
let infoButton = UIButton(type: UIButtonType.InfoDark)
// Left accessory view
let image = UIImage(named: "button50")
let specimenButton = UIButton(type: .Custom)
specimenButton.frame = CGRectMake(0, 0, 30, 30)
specimenButton.setImage(image, forState: .Normal)
annotationView.rightCalloutAccessoryView = infoButton
annotationView.leftCalloutAccessoryView = specimenButton
if currentAnnotation.title == "Empty" {
annotationView.draggable = true
}
return annotationView
}
}
return nil
}
I solved it, by pulling the value into the currentAnnotation, and adding it in a UIImageView on the DetailCalloutAccessoryView
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
guard let subtitle = annotation.subtitle! else { return nil }
if (annotation is SpecimenAnnotation) {
if let annotationView = mapView.dequeueReusableAnnotationViewWithIdentifier(subtitle) {
return annotationView
} else {
let currentAnnotation = annotation as! SpecimenAnnotation
let currentImage = currentAnnotation.objectPoster!
let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: subtitle)
}
annotationView.enabled = true
annotationView.canShowCallout = true
// right accessory view
let calloutInfoButton = UIButton(type: UIButtonType.InfoDark)
// Left accessory view
let calloutLeftImage = UIImage(named: "button50p")
let calloutLeftButton = UIButton(type: .Custom)
calloutLeftButton.frame = CGRectMake(0, 0, 30, 30)
calloutLeftButton.setImage(calloutLeftImage, forState: .Normal)
//show Image on callout Accessory
let url = NSURL(string: currentImage)
let data = NSData(contentsOfURL: url!) //make sure your image in this url does exist, otherwise unwrap in a if let check
//annotationView.image = UIImage(data: data!)
let calloutImage = UIImage(data:data!)
annotationView.detailCalloutAccessoryView = UIImageView(image: calloutImage)
annotationView.rightCalloutAccessoryView = calloutInfoButton
annotationView.leftCalloutAccessoryView = calloutLeftButton
if currentAnnotation.title == "Empty" {
annotationView.draggable = true
}
return annotationView
}
}
return nil
}