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
}
}
}
Related
My Current MKMapView is:
But when I zoom in I want to change Pin Image to this:
and again zoom out change to default violet circle.
I just want to change Pin Image when user zoom In/Out MKMapView in iOS Swift 4.
Thanks In Advance!
Happy Coding!
Step 1. First of all you need to get information which pin/annotation display on you map.
Objective c
-(void)getAnotationsInVisibleMapRectangle
{
NSSet *annotationSet = [myMapView annotationsInMapRect:myMapView.annotationVisibleRect];
NSArray *annotationArray = [annotationSet allObjects];
}
Swift
extension MKMapView {
func visibleAnnotations() -> [MKAnnotation] {
return self.annotationsInMapRect(self.visibleMapRect).map { obj -> MKAnnotation in return obj as! MKAnnotation }
}
}
Step 2. You will have delegate method will call on zoom in/out regionDidChangeAnimated. just call the as above function. And update the Your pin.
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
print("call on zoom and zoom out ")
let aryData = mapView.visibleAnnotations()
print(aryData[0].coordinate)
//remove pin from this coordinate
//And add new pin as you want
}
Here you can just find the pin. And remove old pin and add new pin as you want.
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
I'm trying to do an iOS app using Xcode 6.3 and swift. I use MKMapView for tracking user position. The problem is that if I scroll the map I immediately return to the user position. This is my code:
override func viewDidLoad() {
super.viewDidLoad()
manager = CLLocationManager()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.requestAlwaysAuthorization()
manager.startUpdatingLocation()
theMap.delegate = self
theMap.mapType = MKMapType.Standard
theMap.zoomEnabled = true
theMap.addGestureRecognizer(longPress)
theMap.scrollEnabled = true
}
and
func locationManager(manager:CLLocationManager, didUpdateLocations locations:[AnyObject]) {
let spanX = 0.007
let spanY = 0.007
var newRegion = MKCoordinateRegion(center: theMap.userLocation.coordinate, span: MKCoordinateSpanMake(spanX, spanY))
theMap.setRegion(newRegion, animated: false)
theMap.scrollEnabled = true
}
If I scroll the map, after 1 sec I return to the user position. Should I change setRegion method position?
You need to detect when you scroll the map, possibly by implementing the mapView(_:regionWillChangeAnimated:) method defined in MKMapViewDelegate. In this method you need to set the userTrackingMode property of your map view to .None. Your implementation will be called when the user pans or zooms your theMap variable. So you should strive to keep the implementation as lightweight as possible since it could be called many times for a single pan or zoom gesture.
func mapView(mapView: MKMapView!, regionWillChangeAnimated animated: Bool) {
if you want to stop tracking the user {
mapView.userTrackingMode = .None
}
}
When you want to start following the user's location again, change that property back to either .Follow or .FollowWithHeading:
enum MKUserTrackingMode : Int {
case None // the user's location is not followed
case Follow // the map follows the user's location
case FollowWithHeading // the map follows the user's location and heading
}
I have got mapView, and its delegate method
func mapView(mapView: MKMapView!, didUpdateUserLocation userLocation: MKUserLocation!) {
mapView.userTrackingMode = MKUserTrackingMode.None
var eye = CLLocationCoordinate2DMake(userLocation.coordinate.latitude, userLocation.coordinate.longitude)
var cam = MKMapCamera(lookingAtCenterCoordinate: eye, fromEyeCoordinate: eye, eyeAltitude: 10000)
UIView.animateWithDuration(1, animations: { () -> Void in
mapView.camera = cam
})
}
and it works some random way. it executes when view loads, and than when I am scrolling map to another country or city and zoom, it can(sometimes, now always) return me to my current location. It is curious cause my current location didnt changes and the delegate shouldnt execute.
I need do somehow this thing DONT HAPPENED. So i want to make my map zoom to current location only when location changed, in other way I need to have free map for scrolling.
You have to set you controller as CLLocationManagerDelegate, then declare a variable like:
var locationManager:CLLocationManager = CLLocationManager()
then in your viewDidLoad method:
override func viewDidLoad() {
super.viewDidLoad()
self.locationManager.delegate = self
self.locationManager.stopUpdatingLocation()
It should prevent to call func mapView(mapView: MKMapView!, didUpdateUserLocation userLocation: MKUserLocation!).
Pay attention that you have to re-enbale it in case you need to update map center with your coordinate without manual scrolling to you actual position.
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
}