I am working with a MKMapView that is placed on the third VC (MapViewController: UIViewController) in a tabbed application controller (AppTabController: UITabViewController). If I understand UITabViewController correctly, the MapViewController will not be instantiated until the user navigates to it.
The MKMapView is linked to the MapViewController via the outlet
#IBOutlet weak var mapView: MKMapView!
MapViewController conforms to the MKMapViewDelegate protocol through an extension to MapViewController and I’ve set the MKMapView delegate in the viewDidLoad method of MapViewController.
extension MapViewController: MKMapViewDelegate {
// delegate methods
}
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
}
In viewDidLoad of MapViewController I call showMyLocation() to centre the map on the user’s location in a 1 km. by 1 km. region and show their annotation.
The same method is linked to a “User” button on MapViewController’s nav bar which allows the user to return to their location if they want to.
#IBAction func showMyLocation() {
let region = MKCoordinateRegionMakeWithDistance(mapView.userLocation.coordinate, 1000, 1000)
mapView.setRegion(mapView.regionThatFits(region), animated: true)
}
I am not using a CLLocationManager to determine the user’s location. Because MKMapView is able to return the user’s location to the app.
Answers I've found so far point towards a CLLocationManager approach.
I am a little unsure about what appears to be a timing dependency in obtaining the user’s coordinates from MKMapView.
If I start the app and navigate immediately to the MapViewController tab the coordinates returned by mapView.userLocation.coordinate are 0,0
However if I start the app and wait for a minute or so before going to the MapViewController tab the coordinates are the expected ones for my location.
Finally, if I start the app and navigate immediately to the MapViewController the coordinates are 0,0 as above but if I immediately tap the “User” button the coordinates immediately update to the expected ones without the delay.
My solution so far is to implement the didUpdateUserLocation delegate method to tell me when MKMapView has obtained a valid location for the user.
func mapView(mapView: MKMapView, didUpdateUserLocation userLocation: MKUserLocation) {
if self.rideLocations.isEmpty {
self.showUserLocation()
}
}
My question is have I implemented the necessary pattern to deal with MKMapView user location behaviour timing?
My objective is to have the user’s location properly displayed on the MKMapView regardless of when they navigate to the tab that has the map on it but to avoid overuse of the device location services.
Thanks for all the answers so far - they’ve been a massive help in my Swift / IOS journey!
I know this is an old thread, but I thought I would add my two cents in case it helps someone else. To solve this problem you can implement the mapView delegate method mapView(_:viewFor:) which gets called every time an annotation is added to the map. This includes the user location. Just add the code at the top of the method. It's only called once when the MKUserLocation annotation is added to the mapView. In viewDidLoad disable the button.
`func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?{
if annotation is MKUserLocation {
//return nil so map view draws "blue dot" for standard user location
showMyLocation()
enable your button here.
return nil
}
//other code here.`
Related
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'm trying to figure out how to add a Long press gesture to a map annotation(Mapbox).I have my code set up so that when a user tap's the annotation they segue to another view by putting my code in this function.
func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
}
Now I want to allow a user to segue to another view by holding the same annotation. I tried to use and if and else statement in the code above but the Long press gesture doesn't work unless I tap the annotation first to activate the function so the if and else statement can start working. But I dont want the user to tap then hold. I just want them to either tap or hold down on the annotation.
Thank You for your answers in advance
I'm not familiar with the Mapbox API but if there's no proper delegate method try your own custom implementation with uigesturerecognizer and delegation.
set your gesture recognizer on the annotation view:
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressDetected))
annotation.view.addGestureRecognizer(longPressGestureRecognizer)
make a delegate
weak var delegate: AnnotationViewDelegate?
and a protocol AnnotationViewDelegate in Annotation subclass:
protocol AnnotationViewDelegate: class {
func annotationDidDetectLongPress()
}
implement long press handler and notify the delegate about long tap inside
func longPressDetected(sender: UILongPressGestureRecognizer) {
// here you should notify the delegate
delegate?.annotationDidDetectLongPress()
}
assign delegate to self in controller and implement
func annotationDidDetectLongPress() {
// done
}
I made an app where a Mapbox map is loaded in a view and tracks the user's location with the following code:
func mapView(mapView: MGLMapView, didUpdateUserLocation userLocation: MGLUserLocation?) {
print(userLocation)
}
This code works fine on it's own as a ViewController, but when I add the view to a scrollview with addChildViewController, the code doesn't run. Any ideas?
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.
I am building an iOS app in swift that use the google maps.
The app show the POI near the user, and when I move the map I refresh the POI using a refresh button
#IBAction func refreshPlaces(sender: AnyObject) {
fetchNearbyPlaces(mapView.camera.target)
}
Can I can refresh the POI when I move the map?
Make sure that you declare that your view controller conforms to the GMSMapViewDelegate and that you set self.mapView.delegate = self in that view controller (usually in viewDidLoad).
Then add the rolling method in your view controller:
func mapView(mapView: GMSMapView!, didChangeCameraPosition position: GMSCameraPosition!) {
fetchNearbyPlaces(position.target)
}