I've been breaking my head over a strange piece of behaviour I am noticing when trying to display MKPointAnnotation's on an MKMapView in iOS 10. I found 2 related posts on StackOverflow, but neither actually answers the problem I am facing. They are:
https://stackoverflow.com/questions/36760810/mkmapview-annotation-title-is-not-showing but this does not have any answer, and the issue seems slightly different as here the title NEVER shows while in my case it does show but with a weird workaround.
https://stackoverflow.com/questions/33818285/ios-mapview-annotations-not-showing-for-pins but there the user just had forgotten to actually set a title. There is an interesting comment at the end though by #rockhammer that is somewhat related but not exactly: 'It appears that at the time an annotation is added to a map, the .title must have at least one character, even if it is a " ". Otherwise, even if .title is subsequently modified to be other than "", the label won't show.'
My situation: I have a function where when the user longpresses on the map an annotation will be added. The title is basically the first available line of the address found using a CLGeocoder().reverseGeocodeLocation(...) lookup. If all fails it will simply use the date. You will notice that the annotation is only added all the way at the end when it's absolutely sure there is text in the title. This makes the comment from the 2nd post mentioned above not fit this situation.
My problem: You will notice the line annotation.title = "BLAH" near the top. Without this line the annotation titles will not show up in the MKMapView, but only the pins will show!
#IBOutlet weak var map: MKMapView!
func longPress(gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.state == UIGestureRecognizerState.began {
let touchPoint = gestureRecognizer.location(in: self.map)
let coordinate = map.convert(touchPoint, toCoordinateFrom: self.map)
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
annotation.title = "BLAH" //WITHOUT THIS THE TITLE NEVER SHOWS
CLGeocoder().reverseGeocodeLocation(CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)) { (placemarks, error) in
if error != nil {
print(error!)
} else {
if let placemark = placemarks?[0] {
annotation.title = placemark.subThoroughfare != nil ? placemark.subThoroughfare! : ""
annotation.title = annotation.title! + (annotation.title! == "" ? "" : " ") + (placemark.thoroughfare != nil ? placemark.thoroughfare! : "")
if annotation.title == "" {
annotation.title = placemark.subLocality != nil ? placemark.subLocality! : ""
if annotation.title == "" {
annotation.title = placemark.subAdministrativeArea != nil ? placemark.subAdministrativeArea! : ""
if annotation.title == "" {
annotation.title = placemark.postalCode != nil ? placemark.postalCode! : ""
if annotation.title == "" {
annotation.title = placemark.country != nil ? placemark.country! : ""
}
}
}
}
}
}
if annotation.title == "" {
annotation.title = "Added \(NSDate())"
} //At this point a title is guaranteed to be set. Still, it never shows unless it is first 'initialised' with 'BLAH' or something at the beginning.
}
self.map.addAnnotation(annotation)
}
}
If anyone can show me the logic and how/why this is happening, that'd be wonderful.
You need to change 3 methods which are not correct;
I do not known why this methods do not work.
I propose to you some other methods witch compile and work, normally, well. You need to:
change UIGestureRecognizerState.begantoUIGestureRecognizerState.Began;
change touchPoint = gestureRecognizer.location(in: self.map)to touchPoint = gestureRecognizer.locationInView(self.mapView);
and finally, change coordinate = mapView.convert(teste, toCoordinateFrom: self.mapView)to coordinate = mapView.convertPoint(touchPoint, toCoordinateFromView: self.mapView).
Finally your code should look like this:
#IBOutlet weak var map: MKMapView!
func longPress(gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.state == UIGestureRecognizerState.Began { // And not .began
let touchPoint: CGPoint = gestureRecognizer.locationInView(self.map) //And not location(in: self.map)
//let coordinate = mapView.convert(teste, toCoordinateFrom: self.map)
let coordinate = map.convertPoint(touchPoint, toCoordinateFromView: self.map)
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
//annotation.title = "BLAH" //WITHOUT THIS THE TITLE NEVER SHOWS
CLGeocoder().reverseGeocodeLocation(CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)) { (placemarks, error) in
if error != nil {
print(error!)
} else {
if let placemark = placemarks?[0] {
annotation.title = placemark.subThoroughfare != nil ? placemark.subThoroughfare! : ""
annotation.title = annotation.title! + (annotation.title! == "" ? "" : " ") + (placemark.thoroughfare != nil ? placemark.thoroughfare! : "")
if annotation.title == "" {
annotation.title = placemark.subLocality != nil ? placemark.subLocality! : ""
if annotation.title == "" {
annotation.title = placemark.subAdministrativeArea != nil ? placemark.subAdministrativeArea! : ""
if annotation.title == "" {
annotation.title = placemark.postalCode != nil ? placemark.postalCode! : ""
if annotation.title == "" {
annotation.title = placemark.country != nil ? placemark.country! : ""
}
}
}
}
}
}
if annotation.title == "" {
annotation.title = "Added \(NSDate())"
} //At this point a title is guaranteed to be set. Still, it never shows unless it is first 'initialised' with 'BLAH' or something at the beginning.
}
self.map.addAnnotation(annotation)
}
}
Related
I have a marker on map. When scroll map, then the marker also moves. I can find the marker coordinates, but how to find place information using that coordinate?
Place information of current location
func locate() {
placesClient.currentPlace(callback: { (placeLikelihoodList, error) -> Void in
if let error = error {
print("Pick Place error: \(error.localizedDescription)")
return
}
let placeInfo = getCurrentPlaceInformation()
self.placeNameLbl.text = placeInfo.name
self.placeAddressLbl.text = placeInfo.address
if let placeLikelihoodList = placeLikelihoodList {
let place = placeLikelihoodList.likelihoods.first?.place
if let place = place {
print("LOG: place name : \(place.name), place Address : \(place.formattedAddress)")
PLACE_NAME = place.name
PLACE_ADDRESS = place.formattedAddress ?? ""
let placeInfo = getCurrentPlaceInformation()
self.placeNameLbl.text = placeInfo.name
self.placeAddressLbl.text = placeInfo.address
}
}
})
}
How to find custom coordinates to find place information?
Apple reverse Geocode API
import CoreLocation
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(<#T##location: CLLocation##CLLocation#>, completionHandler: <#T##CLGeocodeCompletionHandler##CLGeocodeCompletionHandler##([CLPlacemark]?, Error?) -> Void#>)
Google reverse Geocode API
Add GoogleMaps to project (can use pods)
let geocoder = GMSGeocoder()
geocoder.reverseGeocodeCoordinate(position) { response, error in
//
if error != nil {
print("reverse geodcode fail: \(error!.localizedDescription)")
} else {
if let places = response?.results() {
if let place = places.first {
if let lines = place.lines {
print("GEOCODE: Formatted Address: \(lines)")
}
} else {
print("GEOCODE: nil first in places")
}
} else {
print("GEOCODE: nil in places")
}
}
}
func getAddrFrmLtLng(latitude:Any, longitude:Any){
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: latitude as! CLLocationDegrees, longitude: longitude as! CLLocationDegrees)
geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in
var placeMark: CLPlacemark!
placeMark = placemarks?[0]
self.displayLocationInfo(placemark: placeMark)
})
}
func displayLocationInfo(placemark: CLPlacemark?) -> String {
var locality = ""
var postalCode = ""
var administrativeArea = ""
var country = ""
var sublocality = ""
var throughfare = ""
var name = ""
if let containsPlacemark = placemark {
//stop updating location to save battery life
// locationManager.stopUpdatingLocation()
locality = (containsPlacemark.locality != nil) ? containsPlacemark.locality! : ""
postalCode = (containsPlacemark.postalCode != nil) ? containsPlacemark.postalCode! : ""
administrativeArea = (containsPlacemark.administrativeArea != nil) ? containsPlacemark.administrativeArea! : ""
country = (containsPlacemark.country != nil) ? containsPlacemark.country! : ""
sublocality = (containsPlacemark.subLocality != nil) ? containsPlacemark.subLocality! : ""
throughfare = (containsPlacemark.thoroughfare != nil) ? containsPlacemark.thoroughfare! : ""
}
var adr: String = ""
if throughfare != "" {
adr = throughfare + ", "
}
if sublocality != "" {
adr = adr + sublocality + ", "
}
if locality != "" {
adr = adr + locality + ", "
}
if administrativeArea != "" {
adr = adr + administrativeArea + ", "
}
if postalCode != "" {
adr = adr + postalCode + ", "
}
if country != "" {
adr = adr + country
}
print(adr)
return adr
}
I have a mapview and I added a method to drop a pin on the location where the user had pressed. The callout shows the address of the location as shown on the image.
screenshot of my mapview with annotation pin and callout view.
And my code is as following:
func onTapGestureRecognized(sender: UILongPressGestureRecognizer) {
self.mapView.removeAnnotations(mapView.annotations)
let location = tapRecognizer.location(in: mapView)
let coordinate = mapView.convert(location,toCoordinateFrom: mapView)
let getLat: CLLocationDegrees = coordinate.latitude
let getLon: CLLocationDegrees = coordinate.longitude
let theLocation: CLLocation = CLLocation(latitude: getLat, longitude: getLon)
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(theLocation, completionHandler: { (placemarks, error) -> Void in
// Place details
var placeMark: CLPlacemark!
placeMark = placemarks?[0]
var theLocationName = ""
var theStreetNumber = ""
var theStreet = ""
var theCity = ""
var theZip = ""
var theCountry = ""
// Address dictionary
print(placeMark.addressDictionary as Any)
// Location name
if let locationName = placeMark.name{
theLocationName = locationName
}
if let streetNumber = placeMark.subThoroughfare{
theStreetNumber = streetNumber
}
// Street address
if let street = placeMark.thoroughfare {
theStreet = street
}
// City
if let city = placeMark.locality {
theCity = city
}
// Zip code
if let zip = placeMark.postalCode{
theZip = zip
}
// Country
if let country = placeMark.isoCountryCode{
theCountry = country
}
let annotation = MKPointAnnotation()
annotation.title = theLocationName
annotation.subtitle = theStreetNumber + " " + theStreet + ", " + theCity + ", " + theCountry + ", " + theZip
if let location = placeMark.location {
annotation.coordinate = location.coordinate
// Display the annotation
self.mapView.showAnnotations([annotation], animated: true)
}
})
}
As you can see, when I try to get the location name by calling the line (((( if let locationName = placeMark.name )))), I can only get the address: "5197 Yonge St", instead of the restaurant name : " Pho 88 Restaurant ".
Can anyone tell me where I did wrong? or is it simply cannot be achieved? Thanks!
I can't give you a complete answer, but I may be able to point you in the right direction. As far as I can see, you will only ever get a single entry returned for placemarks, but you can get a more complete list using MKLocalSearchRequest. the challenge is going to be how you match up the returned values to exactly which one you want - maybe you have to ask the user to select from a short list? Also, I think you need to specify which type of establishment you're searching for. Here's something you could include within your completion handler above
let request = MKLocalSearchRequest()
request.naturalLanguageQuery = "restaurant" // or whatever you're searching for
request.region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: getLat, longitude: getLon), span: self.mapView.region.span)
let search = MKLocalSearch(request: request)
search.start { response, error in
guard let response = response else {
print("There was an error searching for: \(request.naturalLanguageQuery) error: \(error)")
return
}
print("There are \(response.mapItems.count)")
for item in response.mapItems {
// You may be able to match the address to what the geoCode gives you
// or present the user with a list of options
print("\(item.name), \(item.placemark)")
}
}
When I was testing this, the addresses didn't always match up, even when zoomed in - so that geoCoder might give me 1-3 Some Street while the MKLocalSearchRequest returned a restaurant at 3 Some Street
My app has two tabs. On the first tab a bunch of tab annotations are created based on an attraction. When you click on the annotation view, it goes to the 2nd tab which is a detail view controller of the attraction. I am wondering how I can pass the location of the attraction to the detailVC that way I can have the pin at the same address. Here is what I have so far, and there are some inconsistencies, as the address displayed on the detail vc is not equal to the address of the attraction.
func performSearch(input:String) {
attractionsMap.removeAnnotations(attractionsMap.annotations);
matchingItems.removeAll()
let request = MKLocalSearchRequest()
request.naturalLanguageQuery = input
println(input);
request.region = attractionsMap.region;
let search = MKLocalSearch(request: request)
search.startWithCompletionHandler({(response:
MKLocalSearchResponse!,
error: NSError!) in
if error != nil {
println("Error occured in search: \(error.localizedDescription)")
} else if response.mapItems.count == 0 {
println("No matches found")
} else {
println("Matches found")
for item in response.mapItems as! [MKMapItem] {
println("Name = \(item.name)")
println("Phone = \(item.phoneNumber)")
matchingItems.append(item as MKMapItem)
println("Matching items = \(matchingItems.count)")
var placemark = item.placemark;
var subThoroughfare:String = "";
var thoroughfare:String = "";
var locality:String = "";
var postalCode:String = "";
var administrativeArea:String = "";
var country:String = "";
var title = "";
var subtitle = "";
if (placemark.subThoroughfare != nil) {
subThoroughfare = placemark.subThoroughfare;
}
if(placemark.thoroughfare != nil) {
thoroughfare = placemark.thoroughfare;
}
if(placemark.locality != nil) {
locality = placemark.locality;
}
if(placemark.postalCode != nil) {
postalCode = placemark.postalCode;
}
if(placemark.administrativeArea != nil) {
administrativeArea = placemark.administrativeArea;
}
if(placemark.country != nil) {
country = placemark.country;
}
println("viewcontroller placmark data:");
println(locality);
println(postalCode);
println(administrativeArea);
println(country);
title = " \(subThoroughfare) \(thoroughfare) \n \(locality), \(administrativeArea) \n \(postalCode) \(country)";
subtitle = " \(subThoroughfare) \(thoroughfare)";
println(title);
var annotation = MKPointAnnotation()
annotation.coordinate = item.placemark.coordinate
annotation.title = item.name + " " + subtitle;
self.attractionsMap.addAnnotation(annotation)
}
}
})
}
This is the search and adding the pin annotations.
I then attempt to send the attractionLocation below
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var attractionsDetailViewController:AttractionsDetailViewController = segue.destinationViewController as! AttractionsDetailViewController
attractionsDetailViewController.attractionLocation = indicatedMapItem;
}
And then my Detail ViewController to implement the given address:
func getInfo() {
var latitude = attractionLocation.latitude;
var longitude = attractionLocation.longitude;
var latDelta:CLLocationDegrees = 0.000001
var longDelta: CLLocationDegrees = 0.000001
var span: MKCoordinateSpan = MKCoordinateSpanMake(latDelta, longDelta);
var location = CLLocationCoordinate2DMake(latitude, longitude);
var realLocation = CLLocation(latitude: latitude, longitude: longitude);
CLGeocoder().reverseGeocodeLocation(realLocation, completionHandler: { (placemarks, error) -> Void in
var title = ""
var subtitle = ""
var locality = ""
if(error == nil) {
if let placemark = CLPlacemark(placemark: placemarks?[0] as! CLPlacemark) {
var subThoroughfare:String = "";
var thoroughfare:String = "";
var locality:String = "";
var postalCode:String = "";
var administrativeArea:String = "";
var country:String = "";
if (placemark.subThoroughfare != nil) {
subThoroughfare = placemark.subThoroughfare;
}
if(placemark.thoroughfare != nil) {
thoroughfare = placemark.thoroughfare;
}
if(placemark.locality != nil) {
locality = placemark.locality;
}
if(placemark.postalCode != nil) {
postalCode = placemark.postalCode;
}
if(placemark.administrativeArea != nil) {
administrativeArea = placemark.administrativeArea;
}
if(placemark.country != nil) {
country = placemark.country;
}
println("viewcontroller placmark data:");
println(locality);
println(postalCode);
println(administrativeArea);
println(country);
title = " \(subThoroughfare) \(thoroughfare) \n \(locality), \(administrativeArea) \n \(postalCode)\(country)";
subtitle = " \(subThoroughfare) \(thoroughfare)";
println(title);
self.addressLabel.text = title;
}
}
var overallLoc = CLLocationCoordinate2DMake(latitude, longitude);
var region:MKCoordinateRegion = MKCoordinateRegionMake(overallLoc, span);
var annotation = MKPointAnnotation();
annotation.coordinate = location;
annotation.title = subtitle;
self.detailMap.addAnnotation(annotation);
self.detailMap.setRegion(region, animated: true)
})
}
You can store a global variable, which is usually not recommended, or you can pass via segue, which is recommended. It's probably better to not reverse geocode again because reverse geocoding is not always the most accurate method of finding locations...
If you make your attractionLocation property in the detailsVC like this...
var attractionLocation: CLLocation?
var attractionAddress: String?
As far as I can tell, the getInfo function just needs to set the map's region and add the appropriate annotation.
Seeing as the attractionLocation property will be a CLLocation we can access it's coordinate property. And if you pass through the attractionAddress from the first screen then CLGeocoder doesn't need to be used.
func getInfo() {
// Create span for the map region
var latDelta:CLLocationDegrees = 0.000001
var longDelta: CLLocationDegrees = 0.000001
var span: MKCoordinateSpan = MKCoordinateSpanMake(latDelta, longDelta);
// Create region for map
var region:MKCoordinateRegion = MKCoordinateRegionMake(attractionLocation!.coordinate, span);
var annotation = MKPointAnnotation();
annotation.coordinate = attractionLocation!.coordinate;
annotation.title = attractionAddress!;
self.detailMap.addAnnotation(annotation);
self.detailMap.setRegion(region, animated: true)
}
In the past I've not found CLGeocoder to be that consistent when it comes to reverse geocoding. So at least this way you can be sure the address between the two screens will be consistent.
I have application with map where you can make an annotation by dropping a pin. How can I save the annotation, so you can see it when the application is closed and re-opened?
My code's for annotation
func addAnnotation(gesture: UIGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.Began {
var touch = gesture.locationInView(self.Mapa)
var coordinate = Mapa.convertPoint(touch, toCoordinateFromView: self.Mapa)
var location = CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: coordinate.longitude)
var loc = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
CLGeocoder().reverseGeocodeLocation(loc, completionHandler: { (placemarks, error) -> Void in
if error == nil {
let placemark = CLPlacemark(placemark: placemarks[0] as! CLPlacemark)
self.cislo = placemark.subThoroughfare != nil ? placemark.subThoroughfare : ""
self.adresa = placemark.thoroughfare != nil ? placemark.thoroughfare : ""
self.mesto = placemark.subAdministrativeArea != nil ? placemark.subAdministrativeArea : ""
self.krajina = placemark.administrativeArea != nil ? placemark.administrativeArea : ""
let annotation = MKPointAnnotation()
annotation.coordinate = placemark.location.coordinate
annotation.title = self.adresa! + " " + self.cislo!
self.Mapa.addAnnotation(annotation)
println("Špendlík pridaný!")
}
})
}
}
In case you want to see whole code
http://pastebin.com/d89kTrL7
i would save the data into userdefaults as
func addAnnotation(gesture: UIGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.Began {
var touch = gesture.locationInView(self.Mapa)
var coordinate = Mapa.convertPoint(touch, toCoordinateFromView: self.Mapa)
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setDouble(coordinate.longitude, forKey: "longitudeNameKey")
defaults.setDouble(coordinate.latitude, forKey: "latitudeNameKey")
defaults.synchronize()
var location = CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: coordinate.longitude)
var loc = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
CLGeocoder().reverseGeocodeLocation(loc, completionHandler: { (placemarks, error) -> Void in
if error == nil {
let placemark = CLPlacemark(placemark: placemarks[0] as! CLPlacemark)
self.cislo = placemark.subThoroughfare != nil ? placemark.subThoroughfare : ""
self.adresa = placemark.thoroughfare != nil ? placemark.thoroughfare : ""
self.mesto = placemark.subAdministrativeArea != nil ? placemark.subAdministrativeArea : ""
self.krajina = placemark.administrativeArea != nil ? placemark.administrativeArea : ""
let annotation = MKPointAnnotation()
annotation.coordinate = placemark.location.coordinate
annotation.title = self.adresa! + " " + self.cislo!
self.Mapa.addAnnotation(annotation)
println("Špendlík pridaný!")
}
})
}
}
You can save info to NSUserDefaults when the annotations are created.And somewhere in viewDidLoad method you just get all the info from user defaults and then display the annotations.
override func viewDidLoad() {
super.viewDidLoad()
loadAnnotationFromUserDefaults()
}
use loadAnnotationFromUserDefaults method to deserializes the list of coordinates previously saved to NSUserDefaults. Through this method you also load the coordinates as annotations on the map view.
func loadAnnotationFromUserDefaults(){
let defaults = NSUserDefaults.standardUserDefaults()
let long= defaults.doubleForKey("longitudeNameKey")
let lat = defaults.doubleForKey("latitudeNameKey")
println("\(long)")
println("\(lat)")
//You got the coordinates that you lost after terminating now load the coordinates as annotation to mapview
}
You should set new coordinates and terminate application ..notice the coordinates..now again reopen your application ..now you get again those see on the log
P.S code not tested and should change according to your application architecture...only take it as a reference.
Here is the demo project i set up for you
https://drive.google.com/open?id=0B6dTvD1JbkgBRnN2QllWWlJqd0E&authuser=0
The following code seems to print the values twice even though I hold down for 2 seconds.
No matter what duration I change to it always seems to execute twice, does anyone know why this might be?
func action(gestureRecognizer:UIGestureRecognizer){
var touchPoint = gestureRecognizer.locationInView(self.myMap);
var newCo = myMap.convertPoint(touchPoint, toCoordinateFromView: self.myMap);
var annotation = MKPointAnnotation();
annotation.coordinate = newCo;
var loc = CLLocation(latitude: newCo.latitude, longitude: newCo.longitude);
CLGeocoder().reverseGeocodeLocation(loc, completionHandler: {(placemarks, error)->Void in
let pm:CLPlacemark = placemarks[0] as CLPlacemark;
var address = pm.locality + " ," + pm.postalCode + " ," + pm.administrativeArea + " ," + pm.country;
annotation.title = address;
self.myMap.addAnnotation(annotation);
println(address);
println("\(newCo.latitude)");
println("\(newCo.longitude)");
//places.append(["name:":address, "lat": "\(newCo.latitude)", "lon":"\(newCo.longitude)"]);
})
}
Check the state property of the UIGestureRecognizer, you're probably getting both begin and end.
enum UIGestureRecognizerState : Int {
case Possible
case Began
case Changed
case Ended
case Cancelled
case Failed
}
func action(gestureRecognizer:UIGestureRecognizer) {
print("Gesture Recognized")
if gestureRecognizer.state == UIGestureRecognizerState.Ended {
let touchPoint = gestureRecognizer.locationInView(self.map)
let newCoordinate:CLLocationCoordinate2D = self.map.convertPoint(touchPoint, toCoordinateFromView: self.map)
print(newCoordinate)
listNewCoordinates.append(newCoordinate)
let annotation = MKPointAnnotation()
annotation.coordinate.longitude = newCoordinate.longitude
annotation.coordinate.latitude = newCoordinate.latitude
self.map.addAnnotation(annotation)
}
}