Please observe the linked image (bottom, below)
When a user moves the map screen to where the pin ends up near the edge of the screen and then selects that pin annotation, an Apple method then moves the map screen slightly toward the center of the screen so that the pin annotation callout is visible.
My question is twofold:
I. What Apple method is called to make that screen adjustment?
II. What would be the smartest way to implement that same functionality within the mapView didSelect view method if the MKMapViewDelegate extension were implemented?
Mapview screen adjustment sequence:
Theoretically, you can get the frame of the annotation view and check if it's too close to the edges of the map view:
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
// if view.frame is too close to the edges of the MKMapView, triggered your desired method
}
In practice this is unreliable because it's also depends on the callout bubble of the annotation view. We need a diffrent threshold for the "too close to the edges" criteria depending on the size of the callout bubble, for which there's no easy way to do.
This is a little hacky: when the screen adjusts, the regionWillChangeAnimated method will be called. If it is triggered within a split second of tapping on an annotation, there's a good chance that it was caused by user tapping on the annotation:
weak var tappedAnnotationView: MKAnnotationView?
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
self.tappedAnnotationView = view // this property will only be non-nil for 0.1 seconds
DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
self.tappedAnnotationView = nil
}
}
func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
if self.tappedAnnotationView != nil {
print("map adjusted due to tap on an annotation view")
}
}
It works for the basic tests I threw at it, but obviously there will be extreme cases when it break down since it's a hack.
What I came up with is the following:
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
// Get Coordinate for Selected Map Pin
let selectedPinCoordinate = (view.annotation?.coordinate)!
// Determine (X,Y) coordinates for Selected Map Pin on view (mapView)
let SelectedPinMapPoint = mapView.convert(selectedPinCoordinate, toPointTo: mapView)
// Determine (X,Y) coordinate for center point of view (mapView)
let mapCenterMapPoint = mapView.convert(mapView.centerCoordinate, toPointTo: mapView)
// Define an inner view within the existing view frame. Any Selected Map Pin outside
// bounds of inner view is deemed to be too close to the edge of the map for the callout
// to be fully viewable by the user
let innerRectFrameMinX = mapView.frame.maxX * 0.15
let innerRectFrameMaxX = mapView.frame.maxX * 0.85
let innerRectFrameMinY = mapView.frame.maxY * 0.30
let innerRectFrameMaxY = mapView.frame.maxY * 0.85
// Create a variable which will serve as the new center (X,Y) coordinate
var newCenter = CGPoint()
// Default new center (X,Y) coordinate to current view (X,Y) coordinate
newCenter.x = mapCenterMapPoint.x
newCenter.y = mapCenterMapPoint.y
// Determine if x coordinate of Selected Map Pin is outside bounds. If so,
// set a new center x coordinate so callout can be clearly seen by the user
switch (SelectedPinMapPoint.x) {
case _ where (SelectedPinMapPoint.x < innerRectFrameMinX):
newCenter.x = mapView.frame.midX - (innerRectFrameMinX - SelectedPinMapPoint.x)
case _ where (SelectedPinMapPoint.x > innerRectFrameMaxX):
newCenter.x = mapView.frame.midX + (SelectedPinMapPoint.x - innerRectFrameMaxX)
default: break
}
// Determine if y coordinate of Selected Map Pin is outside bounds. If so,
// set a new center y coordinate so callout can be clearly seen by the user
switch (SelectedPinMapPoint.y) {
case _ where (SelectedPinMapPoint.y < innerRectFrameMinY):
newCenter.y = mapView.frame.midY - (innerRectFrameMinY - SelectedPinMapPoint.y)
case _ where (SelectedPinMapPoint.y > innerRectFrameMaxY):
newCenter.y = mapView.frame.midY + (SelectedPinMapPoint.y - innerRectFrameMaxY)
default: break
}
// Convert new map Center (X,Y) coordinate to map coordinate
let newCenterCoordinate = mapView.convert(newCenter, toCoordinateFrom: nil)
// Set new center as center for map view
mapView.setCenter(newCenterCoordinate, animated: true)
}
I've linked a rough graphical sketch of my conception for whatever benefit it may provide.Rough Graphical Sketch
Related
I have a MapView and the user can select a radius to show an area. I add a MKCircle as an overlay. When the radius changes from 30 miles to 1 mile there is a noticeable glitch on the perimeter of the MKCircle while the MapView zooms in. The glitch looks sort of like a flair. It only happens when zooming in and not zooming out.\
Since the zoom constantly changes I remove the old overlay before adding another one but I don't think that's the issue.
How can I remove the glitch on the circle as the MapView's zoom changes?
#IBAction func newRadiusButtonTapped(sender: UIButton) {
// coordinate is the users location and span was 10 miles now it's 1 mile
let region = MKCoordinateRegionMake(location.coordinate, span)
mapView.setRegion(region, animated: true)
let circle = MKCircle(center: location.coordinate, radius: radius)
// remove old overlay before adding another one
for overlay in mapView.overlays {
mapView.remove(overlay)
}
view.layoutIfNeeded()
// mapView.layoutIfNeeded() I tried this but it didn't make a difference
mapView.add(circle)
}
I couldn't find what was causing the glitch but I found this delegate method from this answer on the mapView that gets notified after the mapView region finishes changing. I add the overlay there
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
}
Simple process:
I create a circle property of type MKCircle?
I also create property named shouldAddCircle of type Bool and set it to true.
When the button is pressed I initial the circle property with the MKCircle that I create inside the button and set the shouldAddCircle to true.
Inside the button function I remove all the mapViews overlays.
Inside the delegate method I now check to see that the shouldAddCircle property is true and if it is I then check to see make sure the circle property isn't nil. If they match then I add the initialized circle to the mapView. After I add the circle to the mapView I have to set the shouldAddCircle to false because every time the user scrolls the map regionDidChangeAnimated gets called and it will keep adding overlays to the map.
Here' the code below. Be sure to add mapView.delegate = self in viewDidLoad and to set the MKMapViewDelegate before everything.
var circle: MKCircle?
var shouldAddCircle = true
#IBAction func newRadiusButtonTapped(sender: UIButton) {
// coordinate is the users location and span was 10 miles now it's 1 mile
let region = MKCoordinateRegionMake(location.coordinate, span)
mapView.setRegion(region, animated: true)
let circle = MKCircle(center: location.coordinate, radius: radius)
// set the circle property to match the circle that was just created
self.circle = circle
// set this true
shouldAddCircle = true
// remove old overlay before adding another one
for overlay in mapView.overlays {
mapView.remove(overlay)
}
}
// this function gets called repeatedly as the mapView is zoomed and/or panned
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// make sure this is true because that means the user updated the radius
if shouldAddCircle {
// make sure the circle isn't ni
if let circle = self.circle {
// after the mapView finishes add the circle to it
mapView.add(circle)
// set this to false so that this doesn't called again until the user presses the button where they set it to true
shouldAddCircle = false
}
}
}
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 am using google maps SDK for ios. I want to make a marker fixed in the center of the screen, so when user drags the map, the marker does not move and stays in the center. I am also trying to read the coordinate of the center after the drag. Any input would be appreciated. Thanks!
I would rather display view on top of GMSMapView (do not use markers for that). Since you have screen position for map view, that should be easy to place your view in correct position.
To get coordinates you can use mapView.projection.coordinateForPoint
Documentation is here
To know that drag is finished, make you view controller (or any other object) delegate of map view (GMSMapViewDelegate) and implement mapView:idleAtCameraPosition: method.
Docs are here
Create outlet of GMSMapView and attached Image at the center of the map and search bar at the top to present the location name.
Drag the map on Device, which will trigger 2 delegate method of GMSMapViewDelegate.
func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition)
func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition)
/**
* Called repeatedly during any animations or gestures on the map (or once, if the camera is
* explicitly set). This may not be called for all intermediate camera positions. It is always
* called for the final position of an animation or gesture.
*/
func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition) {
print("didchange")> >
returnPostionOfMapView(mapView: mapView)
}
/**
* Called when the map becomes idle, after any outstanding gestures or animations have completed (or
* after the camera has been explicitly set).
*/
func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition) {
print("idleAt")
//called when the map is idle
returnPostionOfMapView(mapView: mapView)
}
//Convert the location position to address
func returnPostionOfMapView(mapView:GMSMapView){
let geocoder = GMSGeocoder()
let latitute = mapView.camera.target.latitude
let longitude = mapView.camera.target.longitude
let position = CLLocationCoordinate2DMake(latitute, longitude)
geocoder.reverseGeocodeCoordinate(position) { response , error in
if error != nil {
print("GMSReverseGeocode Error: \(String(describing: error?.localizedDescription))")
}else {
let result = response?.results()?.first
let address = result?.lines?.reduce("") { $0 == "" ? $1 : $0 + ", " + $1 }
self.searchBar.text = address
}
}
}
Github Demo Project
What i want is I want to fix the marker position to the centre of my map view. when map moves still my marker should stick to that centre point.
Code for marker is shown below:
var pin:GMSMarker = GMSMarker(position: CLLocationCoordinate2DMake(currentLocation.coordinate.latitude, currentLocation.coordinate.longitude))
pin.icon = UIImage(named: "img_map_pin")
pin.map = mapView?
pin.infoWindowAnchor = CGPointMake(0.44, 0.44)
mapView?.selectedMarker = pin
I want to make same as Uber has done. When map moves marker is in centre and after map stops moving I get that point's address from map.
Assign GMSMapViewDelegate to class and add below code
func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition) {
pin.position = position.target
}
I have a mapView in which I show a location (represented by custom pin) as seen in screenshot
How can I move the mapView so that the icon is visible completely?
To achieve this you first need to calculate the point on screen view where you want to scroll (In your case let assume its 20 pixel up to the annotation marker), Then you need to covert this point to Map location so that you can move map center to that location ;-). Following is the code written in Swift in MKMapView delegate methods where annotation gets tapped.
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
let pinLocation = view.annotation.coordinate
let currentCoordinates = mapView.centerCoordinate // Temporary saved map current center position
// Temp set map center position to pin location
mapView.centerCoordinate = pinLocation
let viewCenter = self.view.center
let fakecenter = CGPoint(x: viewCenter.x, y: viewCenter.y - 20) // point just up to the center point
// convert calculetd point to map location co-ordinates
let coordinate: CLLocationCoordinate2D = mapView.convert(fakecenter, toCoordinateFrom: self.view)
// reset to previous potion so thet animation start from that
mapView.centerCoordinate = currentCoordinates
self.mapView.setCenter(coordinate, animated: true) // change the new center
}