How to fix custom Point Annotations from disappearing from overlap? - ios

Attempting to show custom point annotations from MapKits local search. When the annotations first load on the map all of them show, but then the overlapping ones disappear. And they only reappear when you zoom in on the area.
Many stack solutions have stated to user view?.displayPriority = .required. But for some reason this line of code doesn't work.
Local Search Function on button press
#IBAction func groceryLocalSearch(_ sender: Any) {
self.localStoresArray.removeAll()
self.LocalMapKit.removeAnnotations(self.LocalMapKit.annotations)
currentLocationBtn.isHidden = false
localRequest.naturalLanguageQuery = "Grocery"
//localRequest.region = LocalMapKit.region
self.localSearchBtn.isEnabled = false
let search = MKLocalSearch(request: localRequest)
search.start(completionHandler: {(response, error) in
if error != nil{
print("Error occured when searching: \(error!.localizedDescription)")
} else if response!.mapItems.count == 0 {
print("There were no results in the search")
} else {
print("\(response!.mapItems.count) Results found!")
for item in response!.mapItems {
//Add each item to a array to access in table view
self.localStoresArray.append(item)
let stores = MKPointAnnotation()
stores.title = item.name
stores.coordinate = item.placemark.coordinate
self.LocalMapKit.addAnnotation(stores)
}
}
self.LocalMapKit.showAnnotations(self.LocalMapKit.annotations, animated: true)
self.localStoresTableView.reloadData()
})
View for annotation func
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?{
guard annotation is MKPointAnnotation else { return nil }
let identifier = "Annotation"
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if annotationView == nil {
annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView?.displayPriority = .required
//annotationView?.canShowCallout = true
} else {
annotationView!.annotation = annotation
}
return annotationView
}
I want it so that when the user does the local search it shows all annotations not matter if they are close to each other without having to zoom in.
Image of current map view:

You apparently haven’t set the delegate for your map view because those annotation views are not MKPinAnnotationView, but rather the default MKMarkerAnnotationView. If you’re going to implement MKMapViewDelegate methods, you have to set the delegate of the map view (either in IB or programmatically).
This disappearing act is because the default MKMarkerAnnotationView is configured to enable clustering but you haven’t registered a MKMapViewDefaultClusterAnnotationViewReuseIdentifier.
So, if you really want pin annotation views and you don’t want clustering, set your map view’s delegate and your method should accomplish what you want.
I’d personally suggest you reduce view controller bloat by moving the configuration of your annotation view into a MKPinAnnotationView subclass:
class CustomAnnotationView: MKPinAnnotationView { // or use `MKMarkerAnnotationView` if you want
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
displayPriority = .required
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Then, if you’re targeting iOS 11 and later, in your viewDidLoad, you can register your class, and you don’t have to implement mapView(_:viewFor:) at all:
mapView.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
Or, if you want to enjoy clustering properly, you can expand your CustomAnnotationView:
class CustomAnnotationView: MKPinAnnotationView { // or use `MKMarkerAnnotationView` if you want
static let preferredClusteringIdentifier: String? = Bundle.main.bundleIdentifier! + ".CustomAnnotationView"
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
clusteringIdentifier = CustomAnnotationView.preferredClusteringIdentifier
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var annotation: MKAnnotation? {
didSet {
clusteringIdentifier = CustomAnnotationView.preferredClusteringIdentifier
}
}
}
And then register both your annotation view and a cluster annotation view:
mapView.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
mapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)
Then you enjoy clustering with minimal effort.

Related

Custom Annotation for Mapview Swift

I looked at the posts for this and I still do not receive a custom pin....
Custom Annotation --> this includes setting my image
import UIKit
import MapKit
class CustomPointAnnotation: MKPointAnnotation {
var pinCustomImageName: UIImage!
}
View Controller:
I want to return current location until a button is selected to drop pin
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
//current Location
if !(annotation is CustomPointAnnotation) {
return nil
}
let reuseIdentifier = "pin"
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
annotationView!.canShowCallout = true
} else {
annotationView!.annotation = annotation
}
if let annotationView = annotationView {
annotationView.image = UIImage(named: "Skyscraper")
annotationView.canShowCallout = true
}
return annotationView
}
func addPin() {
pointAnnotation = CustomPointAnnotation()
pointAnnotation.pinCustomImageName = UIImage(named: "Skyscraper")
pointAnnotation.coordinate = currentLocation.coordinate
pointAnnotation.title = "First Building"
pointAnnotation.subtitle = "Latitude: \(currentLocation.coordinate.latitude), \
(currentLocation.coordinate.longitude)"
mapView.addAnnotation(pointAnnotation)
}
There's nothing seriously wrong with the code. But there can be a couple of things that would cause problems, including:
Have you set the delegate (either in IB or programmatically) for the map view? If not, your mapView(_:viewFor:) will never be called. Add breakpoint or debugging print statement to confirm.
Have you confirmed that UIImage(named: "Skyscraper") is successfully retrieving an image? Make sure this is not returning nil.
Note, if only iOS 11 and later, you can simplify this code a bit. Since iOS 11, we no longer need for mapView(_:viewFor:) in simple scenarios like this. I would suggest putting the annotation view configuration code within the annotation view subclass, where it belongs, and avoid cluttering our view controller with a viewFor implementation.
So when you do get the current issue behind you, the recommended process is:
Define classes for your annotation and annotation view:
class CustomAnnotation: MKPointAnnotation {
var pinCustomImage: UIImage!
}
And
class CustomAnnotationView: MKAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
canShowCallout = true
update(for: annotation)
}
override var annotation: MKAnnotation? { didSet { update(for: annotation) } }
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func update(for annotation: MKAnnotation?) {
image = (annotation as? CustomAnnotation)?.pinCustomImage
}
}
In viewDidLoad register this annotation view class:
mapView.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
Remove mapView(_:viewFor:) implementation.
Now when you add a CustomAnnotation to your map’s list of annotations, it will be rendered correctly.
But I would suggest resolving your current problem first. There’s no point in refining you implementation until these more basic issues are resolved.

Is it possible to add an image to a custom MGLAnnotationView in MapBox (iOS, Swift)

Mapbox provides handy documentation on customising an annotation's image and customising an annotation's view:
https://www.mapbox.com/ios-sdk/examples/annotation-views/
https://www.mapbox.com/ios-sdk/examples/marker-image/
However it doesn't seem possible to combine these ideas and customise an annotation view's image. Basically what I am hoping to do is have an annotation of a photo (which the user picks) which also has a boarder which can be animated when tapped.
Has anyone ran into this limitation as well?
MGLAnnotationView inherits from UIView, so most any technique you might use to add an image to a view will also work here.
A simple way would be to add UIImageView as a subview in a custom MGLAnnotationView subclass:
class CustomImageAnnotationView: MGLAnnotationView {
var imageView: UIImageView!
required init(reuseIdentifier: String?, image: UIImage) {
super.init(reuseIdentifier: reuseIdentifier)
self.imageView = UIImageView(image: image)
self.addSubview(self.imageView)
self.frame = self.imageView.frame
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
}
}
And then use that subclass in mapView:viewForAnnotation::
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
guard annotation is MGLPointAnnotation else {
return nil
}
let imageName = "someImageThatYouHaveAddedToYourAppBundle"
// Use the image name as the reuse identifier for its view.
let reuseIdentifier = imageName
// For better performance, always try to reuse existing annotations.
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
// If there’s no reusable annotation view available, initialize a new one.
if annotationView == nil {
annotationView = CustomImageAnnotationView(reuseIdentifier: reuseIdentifier, image: UIImage(named: imageName)!)
}
return annotationView
}
func mapView( _ mapView: MGLMapView, imageFor annotation: MGLAnnotation ) -> MGLAnnotationImage? {
var annotationImage : MGLAnnotationImage? = nil
annotationImage = MGLAnnotationImage( image:UIImage( named: "imageyouwanttoset", reuseIdentifier: annotation.title ) )
return annotationImage
}

AnnotationView Not Displaying

I created a custom annotation view called "PlaceAnnotationView" as shown below:
import Foundation
import MapKit
class PlaceAnnotationView : MKPinAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Then in viewForAnnotation I return my custom annotation view as shown:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
return nil
}
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "PlaceAnnotationView")
if annotationView == nil {
annotationView = PlaceAnnotationView(annotation: annotation, reuseIdentifier: "PlaceAnnotationView")
annotationView?.canShowCallout = true
}
return annotationView
}
Here is the code to add annotations:
private func populateNearByPlaces() {
var region = MKCoordinateRegion()
region.center = CLLocationCoordinate2D(latitude: self.mapView.userLocation.coordinate.latitude, longitude: self.mapView.userLocation.coordinate.longitude)
let request = MKLocalSearchRequest()
request.naturalLanguageQuery = self.selectedCategory
request.region = region
let search = MKLocalSearch(request: request)
search.start { (response, error) in
guard let response = response else {
return
}
for item in response.mapItems {
let annotation = PlaceAnnotation()
annotation.title = item.name
annotation.subtitle = "subtitle"
annotation.mapItem = item
DispatchQueue.main.async {
self.mapView.addAnnotation(annotation)
}
}
}
}
Here is the code for PlaceAnnotationView:
import Foundation
import MapKit
class PlaceAnnotationView : MKPinAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here is the code for PlaceAnnotation:
import Foundation
import MapKit
class PlaceAnnotation : MKPointAnnotation {
var mapItem :MKMapItem!
}
But I don't see any of my annotations being displayed on the map. The viewForAnnotation is fired multiple times for each of my annotation but does not display anything on the screen.
Based on the code that you have (so reluctantly) chosen to reveal, it seems the problem is that you never set the annotation's coordinate. But an annotation's coordinate is crucial. This is how the annotation tells where it is supposed to be in the world, and how the annotation view associated with this annotation knows where on the map to appear. Therefore the annotation view associated with this annotation does not know where on the map to appear. Therefore it doesn't appear.

Why my MKPointAnnotation not draggable and how can I change it?

I have the following code that fetches the gps coordinates from the user and puts a red marker in the place where he currently is:
class ViewController: UIViewController, CLLocationManagerDelegate {
#IBOutlet var mapView: MKMapView!
var locationManager: CLLocationManager?
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager!.delegate = self
if CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse {
locationManager!.startUpdatingLocation()
} else {
locationManager!.requestWhenInUseAuthorization()
}
}
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
switch status {
case .NotDetermined:
print("NotDetermined")
case .Restricted:
print("Restricted")
case .Denied:
print("Denied")
case .AuthorizedAlways:
print("AuthorizedAlways")
case .AuthorizedWhenInUse:
print("AuthorizedWhenInUse")
locationManager!.startUpdatingLocation()
}
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations.first!
let coordinateRegion = MKCoordinateRegionMakeWithDistance(location.coordinate, 500, 500)
mapView.setRegion(coordinateRegion, animated: true)
locationManager?.stopUpdatingLocation()
let annotation = MKPointAnnotation()
annotation.coordinate = location.coordinate
longitude = location.coordinate.longitude
latitude = location.coordinate.latitude
mapView.addAnnotation(annotation)
locationManager = nil
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
print("Failed to initialize GPS: ", error.description)
}
}
That works great and as you can see, I'm using these lines to get the user's lat and long to process it later on:
longitude = location.coordinate.longitude
latitude = location.coordinate.latitude
Now I want to add another functionality - user see his current location on the map as the red pin and can leave it as it is OR - the new feature - he can drag the red guy somewhere else and get its new longitude and latitude.
I've been looking here and there at the draggable feature and I've decided to add this code to my existing one:
func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
if annotation is MKPointAnnotation {
let pinAnnotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "myPin")
pinAnnotationView.pinColor = .Purple
pinAnnotationView.draggable = true
pinAnnotationView.canShowCallout = true
pinAnnotationView.animatesDrop = true
return pinAnnotationView
}
return nil
}
But it didn't do the trick and the pin is still not draggable. How can I fix that and fetch (of for now - print to the console) the new location of the pin each time user grabs it and moves it around?
If you're seeing a red marker rather than the purple marker you specified in your code sample, that would suggest that you haven’t registered your annotation view’s reuse identifier and/or the mapView(_:viewFor:) isn't getting called.
Nowadays, to help avoid view controller bloat, we’d generally put the customization of the annotation view in its own subclass (in the spirit of the “single responsibility principle”):
class CustomAnnotationView: MKPinAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
pinTintColor = .purple
isDraggable = true
canShowCallout = true
animatesDrop = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Then, in iOS 11 and later, you probably would remove the mapView(_:viewFor:) entirely and instead just register that class and you’re done:
override func viewDidLoad() {
super.viewDidLoad()
mapView.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
}
In iOS versions prior to 11 (or if you have some complicated logic where you might have multiple types of annotations that you want to add) you’d want to specify the delegate of the map view and implement mapView(_:viewFor:).
One can hook up the delegate in IB by control-dragging from the delegate outlet in the “Connections Inspector” in IB, or by doing it programmatically, e.g. in viewDidLoad:
mapView.delegate = self
I’d then add a constant to specify my preferred the reuse identifier for the custom annotation view:
class CustomAnnotationView: MKPinAnnotationView {
static let reuseIdentifier = Bundle.main.bundleIdentifier! + ".customAnnotationView"
// these two are unchanged from as shown above
override init(annotation: MKAnnotation?, reuseIdentifier: String?) { ... }
required init?(coder aDecoder: NSCoder) { ... }
}
And then, you’d implement mapView(_:viewFor:) which will attempt to dequeue an previous annotation view and update its annotation, if possible, or instantiate a new annotation view, if not:
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }
if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: CustomAnnotationView.reuseIdentifier) {
annotationView.annotation = annotation
return annotationView
}
return CustomAnnotationView(annotation: annotation, reuseIdentifier: CustomAnnotationView.reuseIdentifier)
}
}
See previous iteration of this answer for older Swift versions.
Tip: You can drag an annotation when it is selected, and it can be selected only when it shows a callout. It can show a callout, when it has a title. So unless you have a title, it won’t work.

How do I make a pin annotation callout in Swift?

I tried to make the callout work but that didn't happen as I did something wrong in my prepare for segue. I want to know how to be able to make a pin annotation callout to another view?
The process of segueing to another scene when the button in the callout is tapped is like so:
Set the delegate of the map view to be the view controller. You can do this either in Interface Builder's "Connections Inspector" or programmatically. You want to specify that the view controller conforms to MKMapViewDelegate, too.
When you create the annotation, make sure to set the title, too:
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
annotation.title = ...
mapView.addAnnotation(annotation)
Define an annotation view subclass with callout with a button:
class CustomAnnotationView: MKPinAnnotationView { // or nowadays, you might use MKMarkerAnnotationView
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
canShowCallout = true
rightCalloutAccessoryView = UIButton(type: .infoLight)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Instruct your MKMapView to use this annotation view. iOS 11 has simplified that process, but I’ll describe how to do it both ways:
If your minimum iOS version is 11 (or later), you’d just register the custom annotation view in as the default and you’re done. You generally don't implement mapView(_:viewFor:) at all in iOS 11 and later. (The only time you might implement that method is if you needed to register multiple reuse identifiers because you had multiple types of custom annotation types.)
override func viewDidLoad() {
super.viewDidLoad()
mapView.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
}
If you need to support iOS versions prior to 11, you would make sure to specify your view controller as the delegate for the MKMapView and then would implement mapView(_:viewFor:):
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }
let reuseIdentifier = "..."
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
if annotationView == nil {
annotationView = CustomAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
} else {
annotationView?.annotation = annotation
}
return annotationView
}
}
For example, that yields a callout something that looks like the following, with the .infoLight button on the right:
Implement calloutAccessoryControlTapped that programmatically performs the segue:
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
performSegue(withIdentifier: "SegueToSecondViewController", sender: view)
}
Obviously, this assumes that you've defined a segue between the two view controllers.
When you segue, pass the necessary information to the destination scene. For example, you might pass a reference to the annotation:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? SecondViewController,
let annotationView = sender as? MKPinAnnotationView {
destination.annotation = annotationView.annotation as? MKPointAnnotation
}
}
For more information, see Creating Callouts in the Location and Maps Programming Guide.
For Swift 2 implementation of the above, see previous revision of this answer.

Resources