Swift - Custom MKAnnotationView, set label title
This link tells me how to create the custom annotation. I am new to IoS app. Trying to understand how to use this in the code.
I want to change the font in the call and have 2 lines instead of one. It appears this class can do all that, but I do not know how to use this in the view controller to change the font. Currently, this is what I have and have created the class as suggested in the link:
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
annotation.title=title
self.map.addAnnotation(annotation)
How do I change this to use the custom class?
THanks
Ram
How do I change this to use the custom class?
You have to understand that there's a difference between an annotation and an annotation view. The former is an object that's associated with some point or region of a map. You add annotations to a map view, and then the map view takes care of figuring out when a particular annotation needs to be displayed so that you don't have to worry about what part of the map is visible, what the zoom level is, etc. You can create subclasses of MKAnnotation if you want to store some kind of custom data in the annotation itself. For example, there's a MKUserAnnotation subclass that adds a heading property; you can do the same kind of thing in your own annotation subclass.
An annotation view provides the visual representation of an annotation. A map view can have hundreds or thousands of annotations, but usually only a handful of those will need to be displayed at any moment. When a map view wants to display an annotation, it calls it's delegate's mapView(_:viewFor:) method, and the delegate returns an appropriate annotation view configured for the annotation in question. If you want to use your own MKAnnotationView subclass, as your question title indicates, then you should implement mapView(_:viewFor:) in your map view delegate such that it instantiates* an instance of your custom MKAnnotationView subclass, configures it, and returns it.
*Before your map view delegate actually creates a new annotation view, you should call the map view's dequeueReusableAnnotationView(withIdentifier:) method, which may return a view that you can use instead of creating a new one. Reusing annotation views is a lot like the way UITableView reuses table cells; in both cases the idea is to avoid constantly creating and destroying short-lived views. This is all explained in the docs, so look there for a complete explanation.
You need to implement the delegate methods for the MKMapView:
import UIKit
import MapKit
extension MapVC: MKMapViewDelegate, CLLocationManagerDelegate
{
func mapView(mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl)
{
...
}
func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
if annotation is MKUserLocation {
//return nil so map view draws "blue dot" for standard user location
return nil
}
let reuseId = "pin"
var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId) as? MKPinAnnotationView
if pinView == nil {
pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
pinView.canShowCallout = true
pinView.animatesDrop = true
pinView.pinColor = .Purple
}
else {
pinView!.annotation = annotation
}
return pinView
}
}
Instead of creating custom annotation view you can use the MKPointAnnotation as you started with your code just set :
annotation.canShowCallout = true
Related
I have custom annotations that sometimes display a textView above them.
They don't display a textView if a variable named text on my annotation is nil.
An annotation may have text to display, but the value of the text variable could change while the annotation is being displayed. In this case I would like the annotation to refresh so that it is no longer displaying the textView.
I already have a delegate function that either creates an annotation with a textView if the annotations text variable is set and creates an annotation without a textView if the text variable of the annotation is not set, it works something like this, although this is not the actual code
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView?{
if annotation is MyCustomAnnotation{
if annotation.hasText(){
return MyCustomAnnotationView(hasText: True)
}else{
return ViewWithoutTextView(hasText: False)
}
}
But if the annotation changes from having text to not having text or vice versa while the annotation is already being displayed, then I don't know how to refresh this or call this again so that the right annotation view is displayed
As #Magnas said in the comment, you would have to remove the annotation and re-add it to update the state.
It would be better to create one custom annotation view that has the logic to handle hiding/showing of the text view inside it. Then you just hold onto a reference of the annotation and update that through the annotationView without going through and messing with map annotations at all.
A rough example (lots of blanks to fill):
// your methods in your custom annotation. Use these wherever you want to change things
class CustomAnnotation: MGLAnnotationView {
func showText() { }
func hideText() { }
}
// Define data structure to access your annotation with some kind of key
dataSourceToAnnotationView: [String: CustomAnnotation]
// save your annotations so you can access them later
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "customReuseId")
if annotationView == nil {
annotationView = CustomAnnotation()
let key = "exampleKeyString"
dataSourceToAnnotationView[key] = annotationView as! CustomAnnotation
}
return annotationView
}
I am writing an app that displays a set of thumbnail images on a map. When I need to display a new thumbnail I dequeue a map annotationview using dequeueReusableAnnotationViewWithIdentifier. I then change the annotationview image. There appears to be an animation effect that fades from the image that was used by the previous use of the annotationview to the newly assigned image. Is there an easy way to just display the new image without the animation.
In:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation)
I have tried:
MKAnnotationView(annotation: annotation, reuseIdentifier: nil)
i.e. create a new annotation view that will not be reused after it is scrolled off the screen or is removed by the app. This resolves the problem but annotation views will never be available for dequeueReusableAnnotationView. For performance reasons this is not recommended but it fixes my problem! Is this the only solution?
In your MKAnnotationView subclass, you should have something like
override var annotation: MKAnnotation? {
willSet {
...
}
}
In case an MKAnnotationView instance is not needed any more, MapKit sets annotation = nil.
This is an excellent time in willSet to clean up the objct: set the image to nil, maybe set other properties that you use to nil also.
If you are doing it this way, there should be no image referenced that could be shown.
This is my first foray into the app world so it's taken a lot of research to get to this point. I'm building a map application and am going for an interface similar to the zillow app seen below. I am trying to come up with the right approach that allows me to click on a map annotation and it brings up a smaller view where I can interact with it. So essentially I have a few questions:
Should I use a subview inside the map controller, or use a container view. Or is there another approach I haven't seen?
How do I push data from the annotation to that function?
How do I keep this subview hidden until an annotation has been clicked?
So far this is the closest thing I can find: Customize MKAnnotation Callout View?
Thanks!
I am new to iOS also, but I have done something similar to what you want to do. I have a view that shows some statistics, speed, bearing, etc. When someone clicks on the annotation, I toggle showing and hiding the statistics. There may be better ways, but here is what I did.
"Executive summary for your questions"
1 and 3) Use a subview over the map that you hide and unhide
2) Subclass both MKAnnotation and MKAnnotationView. Put the data you want to pass in a property of the subclassed MKAnnotationView, and then transfer the property to the MKAnnotationView when you create it. You can then retrieve it from the view passed in to didSelectAnnotationView.
Details
1) and 3) I created a subview that sits on the mapView and set it as hidden in the story board initially. I then have a toggeleMarkerStatistics() func
tion that toggles the visibility of the view. So something like this
func toggleMarkerStatistics() {
if mapMarkerStatistics.hidden {
mapMarkerStatistics.hidden = false
} else {
mapMarkerStatistics.hidden = true
}
}
This function is called from within
func mapView(mapView: MKMapView, didSelectAnnotationView view: MKAnnotationView) { }
2) To get data into the didSelectAnnotationView, here is what I did.
I subclassed both MKAnnotation and MKAnnotationView and added properties to hold the data that I wanted to pass to didSelectAnnotationView. So something like this:
class MyAnnotation: MKPointAnnotation {
var myMarker: MyMapMarker?
}
class MyMKAnnotationView: MKAnnotationView {
var myMarker: MyMapMarker?
}
When you create the annotation, set the property, before you add the annotation to the map.
let annotation = MyAnnotation()
annotation.myMarker = marker
annotation.coordinate = location
annotation.title = "btw, you MUST have a title or bad things happen"
mapView.addAnnotation(annotation)
Then in viewForAnnotation, you will be given your custom annotation with the property you set after you created it and you are asked to create a view for this annotation. Now when you create this view, set the view property to annotation property before you return the view. Now the data you want to pass to didSelectAnnotationView will be available on the view passed to didSelectAnnotationView
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
let view: MKAnnotationView! = mapView.dequeueReusableAnnotationViewWithIdentifier("marker") ?? MyMKAnnotationView(annotation: annotation, reuseIdentifier: "marker")
view.annotation = annotation
view.canShowCallout = false // you need this to make this work
if let annotation = annotation as? MyAnnotation {
if let view = view as? MyMKAnnotationView {
view.myMarker = annotation.myMarker
}
}
return view
}
Now in didSelectAnnotationView, retrieve the data you set when you created the annotation:
func mapView(mapView: MKMapView, didSelectAnnotationView view: MKAnnotationView) {
if let view = view as? MyMKAnnotationView {
if let marker = view.myMarker {
toggleMarkerStatistics() // hide or unhide the view
// do something with your data
}
}
}
Note:
I tried to copy and simplify this from my code, which actually tries to support both Apple and Google maps, so hopefully I don't have any typo's, but I think is a good representation of the steps I take.
A few more things to note:
I think you must provide a title for the annotaion
I think you must set the view's canShowCallout to false
I think both of these requirements can be found in the documentation, but I don't have a pointer to this right now.
Above is an example of something I am trying to do. I have two images, "marker.png" and "userImage.png". I can get each to display individually as an annotation, but I want to overlay the userImage on the marker background as one annotation. It seems the picture above does something like this with the white marker and a picture on top. I see MKAnnotationView takes a UIImage as a property but no UIImageView. How can I accomplish what I am trying to do?
Currently I am able to just show one image
func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
if !(annotation is MKPointAnnotation) {
return nil
}
let reuseId = "CustomMarker"
var markerView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId)
if markerView == nil {
markerView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
markerView.image = UIImage(named: "marker.png")
}
else {
markerView!.annotation = annotation
}
return markerView
}
Implement your own MKAnnotationView subclass. Now you've got a view that's all yours! It can have whatever subviews you want, or (this is what I would do) you can just implement drawRect: and draw into it; drawing a composite or arrangement of images in drawRect: is easy.
Or, you could just keep doing what you're doing but composite the images in code and use the resulting combined image as the image property. That's easy too.
I have a working loop to setup annotations for the title and subtitle elements for some working data points. What I want to do within that same loop structure is to set the pin color to Purple instead of the default. What I can't figure out is what I need to do to tap into my theMapView to set the pin accordingly.
My working loop and some attempts at something...
....
for var index = 0; index < MySupplierData.count; ++index {
// Establish an Annotation
myAnnotation = MKPointAnnotation();
... establish the coordinate,title, subtitle properties - this all works
self.theMapView.addAnnotation(myAnnotation) // this works great.
// In thinking about PinView and how to set it up I have this...
myPinView = MKPinAnnotationView();
myPinView.animatesDrop = true;
myPinView.pinColor = MKPinAnnotationColor.Purple;
// Now how do I get this view to be used for this particular Annotation in theMapView that I am iterating through??? Somehow I need to marry them or know how to replace these attributes directly without the above code for each data point added to the view
// It would be nice to have some kind of addPinView.
}
You need to implement the viewForAnnotation delegate method and return an MKAnnotationView (or subclass) from there.
This is just like in Objective-C -- the underlying SDK works the same way.
Remove the creation of MKPinAnnotationView from the for loop that adds the annotations and implement the delegate method instead.
Here is a sample implementation of the viewForAnnotation delegate method in Swift:
func mapView(mapView: MKMapView!,
viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
if annotation is MKUserLocation {
//return nil so map view draws "blue dot" for standard user location
return nil
}
let reuseId = "pin"
var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId) as? MKPinAnnotationView
if pinView == nil {
pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
pinView!.canShowCallout = true
pinView!.animatesDrop = true
pinView!.pinColor = .Purple
}
else {
pinView!.annotation = annotation
}
return pinView
}