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.
Related
I have the following function in an extension of MKMapView that let me reload a specific annotation and keep it selected if needed and the issue is every time the self.removeAnnotation is called, the whole map is reloaded (or at least all the pins "jumped" as if a reload occurred)
Is there a way to achieve a reload of a single annotation without having the visual of a whole map reloading ?
func reloadAnnotation(_ annotation: MKAnnotation, keepSelection: Bool = true) {
let isSelected = selectedAnnotations.contains(where: annotation.isEqual(_:))
//already tried **UIView.performWithoutAnimation** which decrease the jumping effect
// UIView.performWithoutAnimation {
self.removeAnnotation(annotation)
self.addAnnotation(annotation)
// }
guard isSelected && keepSelection else {
return
}
self.selectedAnnotations.append(annotation)
}
In my experience the unwanted "whole map reloading" visual effect comes from the recalculation of clusters which is triggered by self.removeAnnotation(annotation) and self.addAnnotation(annotation).
So you have to avoid this methods if you just want to update some visual information.
My assumption is that your callout and/or title changed dynamically while selected and you reload because you want to render the changed callout information.
func reloadAnnotation(_ annotation: MKAnnotation, keepSelection: Bool = true) {
let annotationView = mapView.view(for: annotation) as? MyCoolAnnotationView
if let annotationView {
// do stuff like
annotationView.setNeedsLayout()
}
}
annotationView will be nil if the annotation is not in the visible region or part of a cluster. In that case you don't want to reload anyways.
Instead of annotationView.setNeedsLayout() you might call your own method that does whatever you want.
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 posted another question with the same code, but this question is different.
I want to add buttons in the bottom right corner of my Speech bubble that displays
Hello World!
Welcome to my marker!
I want to know how to place the buttons there, but if you want to know what the buttons would do, one of them would keep track of how many upvotes the bubble got by other users, and the other would send a request to another user.
Also, I found this example that looks like it implements a different version of a speech bubble(popup) that may be better to use
import Mapbox
class ViewController: UIViewController, MGLMapViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let mapView = MGLMapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// Set the map’s center coordinate and zoom level.
mapView.setCenter(CLLocationCoordinate2D(latitude: 40.7326808, longitude: -73.9843407), zoomLevel: 12, animated: false)
view.addSubview(mapView)
// Set the delegate property of our map view to `self` after instantiating it.
mapView.delegate = self
// Declare the marker `hello` and set its coordinates, title, and subtitle.
let hello = MGLPointAnnotation()
hello.coordinate = CLLocationCoordinate2D(latitude: 40.7326808, longitude: -73.9843407)
hello.title = "Hello world!"
hello.subtitle = "Welcome to my marker"
// Add marker `hello` to the map.
mapView.addAnnotation(hello)
}
// Use the default marker. See also: our view annotation or custom marker examples.
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
return nil
}
// Allow callout view to appear when an annotation is tapped.
func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
return true
}
}
Below is what I would like my expected output to look like approximately
If you want to use the built-in Mapbox callout you may want to consider implementing the -mapView:rightCalloutAccessoryViewForAnnotation:
delegate method that allows you to further customize an MGLCallout as illustrated in this example: https://www.mapbox.com/ios-sdk/maps/examples/default-callout/. That delegate method returns a UIView so you can customize the UIView however you'd like to contain the buttons you want.
You'll notice in the example that another delegate method, -mapView:annotation:calloutAccessoryControlTapped: is also implemented. This gets called when the right callout accessory view (returned by -mapView:rightCalloutAccessoryViewForAnnotation:) is selected, so you could adapt this by placing your logic in that delegate method when a user selects the right side of the callout view.
I have a TableView that is used to show MapView annotation callouts when the cells are tapped.
In iOS 10 I can centre the MapView on an annotation then show it's callout using:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let location = locations[indexPath.item]
mapView.setCenter(location.coordinate, animated: true)
mapView.selectAnnotation(location, animated: true)
}
locations is an array of MKAnnotations. I'm using MKPinAnnotationViews on iOS 10 and MKMarkerAnnotationViews on iOS 11.
iOS 11 automatically hides and shows MKMarkerAnnotationViews as you zoom the map.
This has the unfortunate side effect of preventing .selectAnnotation() from working reliably because the marker could still be hidden after centering the map.
I've seen the docs and understand why:
If the specified annotation is not onscreen, and therefore does not
have an associated annotation view, this method has no effect.
Is there a way to disable annotation clustering/hiding?
Or some way to force the selected annotation to be visible?
You can set the displayPriority of an MKMarkerAnnotationView to a rawValue of 1000 and the less interesting MKMarkerAnnotationView's displayPriority to something lower. This will cause that marker annotation to take precedence over others.
In your case, you will like want to hold a reference to the annotation that you would like to select, remove that annotation from the map view and add it again. This will cause the map view to request a view for the annotation again and you can adjust the priority so that it is higher than the annotations around it. For example:
func showAnnotation()
{
self.specialAnnotation = annotations.last
self.mapView.removeAnnotation(self.specialAnnotation)
self.mapView.addAnnotation(self.specialAnnotation)
self.mapView.setCenter(self.specialAnnotation.coordinate, animated: true)
self.mapView.selectAnnotation(self.specialAnnotation, animated: true)
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?
{
let markerView = mapView.dequeueReusableAnnotationView(withIdentifier: "Marker", for: annotation) as? MKMarkerAnnotationView
let priority = (annotation as? Annotation) == self.specialAnnotation ? 1000 : 500
markerView?.displayPriority = MKFeatureDisplayPriority(rawValue: priority)
// optionally change the tint color for the selected annotation
markerView?.markerTintColor = priority == 1000 ? .blue : .red
return markerView
}
Where specialAnnotation is an object that conforms to MKAnnotation.
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