Prevent scrolling in a MKMapView, also when zooming - iphone

The scrollEnabled seems to be breakable once the user starts pinching in a MKMapView.
You still can't scroll with one finger, but if you scroll with two fingers while zooming in and out, you can move the map.
I have tried :
Subclassing the MKMapKit to disable the scroll view inside it.
Implementing –mapView:regionWillChangeAnimated: to enforce the center.
Disabling scrollEnabled.
but with no luck.
Can anyone tell me a sure way to ONLY have zooming in a MKMapView, so the center point always stays in the middle ?

You can try to handle the pinch gestures yourself using a UIPinchGestureRecognizer:
First set scrollEnabled and zoomEnabled to NO and create the gesture recognizer:
UIPinchGestureRecognizer* recognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self
action:#selector(handlePinch:)];
[self.mapView addGestureRecognizer:recognizer];
In the recognizer handler adjust the MKCoordinateSpan according to the zoom scale:
- (void)handlePinch:(UIPinchGestureRecognizer*)recognizer
{
static MKCoordinateRegion originalRegion;
if (recognizer.state == UIGestureRecognizerStateBegan) {
originalRegion = self.mapView.region;
}
double latdelta = originalRegion.span.latitudeDelta / recognizer.scale;
double londelta = originalRegion.span.longitudeDelta / recognizer.scale;
// TODO: set these constants to appropriate values to set max/min zoomscale
latdelta = MAX(MIN(latdelta, 80), 0.02);
londelta = MAX(MIN(londelta, 80), 0.02);
MKCoordinateSpan span = MKCoordinateSpanMake(latdelta, londelta);
[self.mapView setRegion:MKCoordinateRegionMake(originalRegion.center, span) animated:YES];
}
This may not work perfectly like Apple's implementation but it should solve your issue.

Swift 3.0 version of #Paras Joshi answer https://stackoverflow.com/a/11954355/3754976
with small animation fix.
class MapViewZoomCenter: MKMapView {
var originalRegion: MKCoordinateRegion!
override func awakeFromNib() {
self.configureView()
}
func configureView() {
isZoomEnabled = false
self.registerZoomGesture()
}
///Register zoom gesture
func registerZoomGesture() {
let recognizer = UIPinchGestureRecognizer(target: self, action:#selector(MapViewZoomCenter.handleMapPinch(recognizer:)))
self.addGestureRecognizer(recognizer)
}
///Zoom in/out map
func handleMapPinch(recognizer: UIPinchGestureRecognizer) {
if (recognizer.state == .began) {
self.originalRegion = self.region;
}
var latdelta: Double = originalRegion.span.latitudeDelta / Double(recognizer.scale)
var londelta: Double = originalRegion.span.longitudeDelta / Double(recognizer.scale)
//set these constants to appropriate values to set max/min zoomscale
latdelta = max(min(latdelta, 80), 0.02);
londelta = max(min(londelta, 80), 0.02);
let span = MKCoordinateSpanMake(latdelta, londelta)
self.setRegion(MKCoordinateRegionMake(originalRegion.center, span), animated: false)
}
}

Try implementing –mapView:regionWillChangeAnimated: or –mapView:regionDidChangeAnimated: in your map view's delegate so that the map is always centered on your preferred location.

I've read about this before, though I've never actually tried it. Have a look at this article about a MKMapView with boundaries. It uses two delegate methods to check if the view has been scrolled by the user.
http://blog.jamgraham.com/blog/2012/04/29/adding-boundaries-to-mkmapview
The article describes an approach which is similar to what you've tried, so, sorry if you've already stumbled upon it.

I did not have a lot of luck with any of these answers. Doing my own pinch just conflicted too much. I was running into cases where the normal zoom would zoom farther in than I could do with my own pinch.
Originally, I tried as the original poster to do something like:
- (void) mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
MKCoordinateRegion region = mapView.region;
//...
// adjust the region.center
//...
mapView.region = region;
}
What I found was that that had no effect. I also discovered through NSLogs that this method will fire even when I set the region or centerCoordinate programmatically. Which led to the question: "Wouldn't the above, if it DID work go infinite?"
So I'm conjecturing and hypothesizing now that while user zoom/scroll/rotate is happening, MapView somehow suppresses or ignores changes to the region. Something about the arbitration renders the programmatic adjustment impotent.
If that's the problem, then maybe the key is to get the region adjustment outside of the regionDidChanged: notification. AND since any adjustment will trigger another notification, it is important that it be able to determine when not to adjust anymore. This led me to the following implementation (where subject is supplying the center coordinate that I want to stay in the middle):
- (void) recenterMap {
double latDiff = self.subject.coordinate.latitude self.mapView.centerCoordinate.latitude;
double lonDiff = self.subject.coordinate.longitude - self.mapView.centerCoordinate.longitude;
BOOL latIsDiff = ABS(latDiff) > 0.00001;
BOOL lonIsDiff = ABS(lonDiff) > 0.00001;
if (self.subject.isLocated && (lonIsDiff || latIsDiff)) {
[self.mapView setCenterCoordinate: self.subject.coordinate animated: YES];
}
}
- (void) mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
if (self.isShowingMap) {
if (self.isInEdit) {
self.setLocationButton.hidden = NO;
self.mapEditPrompt.hidden = YES;
}
else {
if (self.subject.isLocated) { // dispatch outside so it will happen after the map view user events are done
dispatch_after(DISPATCH_TIME_NOW, dispatch_get_main_queue(), ^{
[self recenterMap];
});
}
}
}
}
The delay where it slides it back can vary, but it really does work pretty well. And lets the map interaction remain Apple-esque while it's happening.

I tried this and it works.
First create a property:
var originalCenter: CLLocationCoordinate2D?
Then in regionWillChangeAnimated, check if this event is caused by a UIPinchGestureRecognizer:
func mapView(mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
let firstView = mapView.subviews.first
if let recognizer = firstView?.gestureRecognizers?.filter({ $0.state == .Began || $0.state == .Ended }).first as? UIPinchGestureRecognizer {
if recognizer.scale != 1.0 {
originalCenter = mapView.region.center
}
}
}
Then in regionDidChangeAnimated, return to original region if a pinch gesture caused the region changing:
func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if let center = originalCenter {
mapView.setRegion(MKCoordinateRegion(center: center, span: mapView.region.span), animated: true)
originalCenter = nil
return
}
// your other code
}

Related

View blocked from moving after being rotated

I implemented touchesMoved method for moving my view around and RotationGestureRecognizer to rotate it. It works normal, I can move and rotate my view. The problem is that the view after being rotated cannot be moved anymore. It pins itself to center and doesn't go anywhere.
Here are the gifs as a visual description of the problem:
View is moved around its superview
View won't move after rotation
touchesMoved methode:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let sv = superview, let touch = touches.first else { return }
let parentFrame = sv.bounds
let location = touch.location(in: self)
let previousLocation = touch.previousLocation(in: self)
var newFrame = self.frame.offsetBy(dx: location.x - previousLocation.x, dy: location.y - previousLocation.y)
newFrame.origin.x = max(newFrame.origin.x, 0.0)
newFrame.origin.x = min(newFrame.origin.x, parentFrame.size.width - newFrame.size.width)
newFrame.origin.y = max(newFrame.origin.y, 0.0)
newFrame.origin.y = min(newFrame.origin.y, parentFrame.size.height - newFrame.size.height)
self.frame = newFrame
}
RotationGestureRecognizer method:
#objc func rotationGestureHandler(recognizer:UIRotationGestureRecognizer) {
if let view = recognizer.view {
view.transform = view.transform.rotated(by: recognizer.rotation)
print(view.frame)
recognizer.rotation = 0
}
}
Any ideas why this can happen?
Thanks to everybody in advance
yourGestureRecognizer.cancelsTouchesInView = true (this is default value)
Because your Rotate Gesture "cancel" your touch in this view, so the touchMoved isn't triggered.
When this property is true (the default) and the gesture recognizer recognizes its gesture, the touches of that gesture that are pending aren’t delivered to the view and previously delivered touches are canceled through a touchesCancelled(_:with:) message sent to the view. If a gesture recognizer doesn’t recognize its gesture or if the value of this property is false, the view receives all touches in the multi-touch sequence.
Check this here: https://developer.apple.com/documentation/uikit/uigesturerecognizer/1624218-cancelstouchesinview

NSViewController slow to register mouse events?

I've been working on a small image uploading menubar app for OS X. I've created custom NSView subclass for the uploaded items.
Here's what it looks like by default:
Mouse events are handled by the view's NSViewController in the following way:
import Cocoa
class MenuItemController: NSViewController {
private var trackingArea: NSTrackingArea?
override func mouseEntered(theEvent: NSEvent) {
if let v = self.view as? MenuItemView {
v.shouldHighlight = true
v.needsDisplay = true
}
}
override func mouseExited(theEvent: NSEvent) {
if let v = self.view as? MenuItemView {
v.shouldHighlight = false
v.needsDisplay = true
}
}
override func viewDidLoad() {
super.viewDidLoad()
if (trackingArea == nil) {
trackingArea = NSTrackingArea(rect: self.view.bounds, options: [.ActiveAlways, .MouseEnteredAndExited], owner: self, userInfo: nil)
self.view.addTrackingArea(trackingArea!)
}
/* rest of the code... */
}
}
It works fine until I move my cursor fast over the items. It seems like the mouseExited() event is not called, and the view remains with a blue background (mouse is actually on the Quit button):
I also tried moving the mouse handling into the NSView, but with same results. I appreciate any input! Thanks!
In my opinion, Apple has had bugs in this area.
Assuming you update your tracking area according to apple docs,
adding this additional fix might fix your problem... it fixes it for me in many cases.
I verify in mouseMoved / mouseEntered routine that the mouse cursor is still within my views frame, and if not, call mouseExited: myself.
- (void) adjustTrackingArea
{
if ( trackingArea )
{
[self removeTrackingArea:trackingArea];
[trackingArea release];
}
// determine the tracking options
NSTrackingAreaOptions trackingOptions = // NSTrackingEnabledDuringMouseDrag | // don't track during drag
NSTrackingMouseMoved |
NSTrackingMouseEnteredAndExited |
//NSTrackingActiveInActiveApp | NSTrackingActiveInKeyWindow | NSTrackingActiveWhenFirstResponder |
NSTrackingActiveAlways;
NSRect theRect = [self visibleRect];
trackingArea = [[NSTrackingArea alloc]
initWithRect: theRect
options: trackingOptions
owner: self
userInfo: nil];
[self addTrackingArea:trackingArea];
}
- (void)resetCursorRects
{
[self adjustTrackingArea];
}
- (void)mouseEntered:(NSEvent *)ev
{
[self setNeedsDisplay:YES];
// make sure current mouse cursor location remains under the mouse cursor
NSPoint cursorPt = [self convertPoint:[[self window] mouseLocationOutsideOfEventStream] fromView:NULL];
// apple bug!!!
//NSPoint cursorPt2 = [self convertPointFromBase:[ev locationInWindow]];
//if ( cursorPt.x != cursorPt2.x )
// NSLog( #"hello old cursorPt" );
NSRect r = [self frame];
if ( cursorPt.x > NSMaxX( r ) || cursorPt.x < 0 )
{
[self mouseExited:ev];
//cursorPt.x = [self convertPointFromBase:[ev locationInWindow]];
//if ( cursorPt.x > NSMaxX( r ) || cursorPt.x < r.origin.x )
return;
}
... your custom stuff here ...
}
- (void)mouseExited:(NSEvent *)theEvent
{
if ( isTrackingCursor == NO )
return;
[[NSCursor arrowCursor] set];
isTrackingCursor = NO;
[self setNeedsDisplay:YES];
}
- (void)mouseMoved:(NSEvent *)theEvent
{
[self mouseEntered:theEvent];
}
I do not understand whether you are installing an NSTrackingArea for the whole window (in this case menu) or for each item. If you are doing the latter, don't do it, or you will spend an endless time to correct the problems you are seeing. The way I handled this buggy behaviour is to create an NSTrackingArea for the whole window and I figure myself where the mouse is and handle the highlighting of each item myself. I know this is not ideal, but this was the only way I was able to solve it after knocking myself in the head for three days.
You can track which was the old active menuitem and set manage it accordingly, something like:
override func mouseEntered(theEvent: NSEvent) {
if let v = self.view as? MenuItemView {
lastEntered.shouldHighLight = false
lastEntered.needsDisplay = true
lastEntered = v;
lastEntered.shouldHighLight = true
lastEntered.needsDisplay = true
}
}
Thus you will ensure that at most one will be active.

Is there way to limit MKMapView maximum zoom level?

the question is - is there a way to limit maximum zoom level for MKMapView? Or is there a way to track when user zooms to the level where there's no map image available?
If you're working with iOS 7+ only, there's a new camera.altitude property that you can get/set to enforce a zoom level. Its equivalent to azdev's solution, but no external code is required.
In testing, I also discovered that it was possible to enter an infinite loop if you repeatedly tried to zoom in at detail, so I have a var to prevent that in my code below.
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
// enforce maximum zoom level
if (_mapView.camera.altitude < 120.00 && !_modifyingMap) {
_modifyingMap = YES; // prevents strange infinite loop case
_mapView.camera.altitude = 120.00;
_modifyingMap = NO;
}
}
You could use the mapView:regionWillChangeAnimated: delegate method to listen for region change events, and if the region is wider than your maximum region, set it back to the max region with setRegion:animated: to indicate to your user that they can't zoom out that far. Here's the methods:
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated
I just spent some time working on this for an app i'm building. Here's what I came up with:
I started with Troy Brant's script on this page which is a nicer way to set the map view I think.
I added a method to return the current zoom level.
In MKMapView+ZoomLevel.h:
- (double)getZoomLevel;
In MKMapView+ZoomLevel.m:
// Return the current map zoomLevel equivalent, just like above but in reverse
- (double)getZoomLevel{
MKCoordinateRegion reg=self.region; // the current visible region
MKCoordinateSpan span=reg.span; // the deltas
CLLocationCoordinate2D centerCoordinate=reg.center; // the center in degrees
// Get the left and right most lonitudes
CLLocationDegrees leftLongitude=(centerCoordinate.longitude-(span.longitudeDelta/2));
CLLocationDegrees rightLongitude=(centerCoordinate.longitude+(span.longitudeDelta/2));
CGSize mapSizeInPixels = self.bounds.size; // the size of the display window
// Get the left and right side of the screen in fully zoomed-in pixels
double leftPixel=[self longitudeToPixelSpaceX:leftLongitude];
double rightPixel=[self longitudeToPixelSpaceX:rightLongitude];
// The span of the screen width in fully zoomed-in pixels
double pixelDelta=abs(rightPixel-leftPixel);
// The ratio of the pixels to what we're actually showing
double zoomScale= mapSizeInPixels.width /pixelDelta;
// Inverse exponent
double zoomExponent=log2(zoomScale);
// Adjust our scale
double zoomLevel=zoomExponent+20;
return zoomLevel;
}
This method relies on a few private methods in the code linked above.
I added this in to my MKMapView delegate (as #vladimir recommended above)
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
NSLog(#"%f",[mapView getZoomLevel]);
if([mapView getZoomLevel]<10) {
[mapView setCenterCoordinate:[mapView centerCoordinate] zoomLevel:10 animated:TRUE];
}
}
This has the effect of re-zooming if the user gets too far out. You can use regionWillChangeAnimated to prevent the map from 'bouncing' back in.
Regarding the looping comments above, it looks like this method only iterates once.
Yes, this is doable. First, extend MKMapView by using MKMapView+ZoomLevel.
Then, implement this in your MKMapViewDelegate:
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
// Constrain zoom level to 8.
if( [mapView zoomLevel] < 8 )
{
[mapView setCenterCoordinate:mapView.centerCoordinate
zoomLevel:8
animated:NO];
}
}
Here is code rewritten in Swift 3 using MKMapView+ZoomLevel and #T.Markle answer:
import Foundation
import MapKit
fileprivate let MERCATOR_OFFSET: Double = 268435456
fileprivate let MERCATOR_RADIUS: Double = 85445659.44705395
extension MKMapView {
func getZoomLevel() -> Double {
let reg = self.region
let span = reg.span
let centerCoordinate = reg.center
// Get the left and right most lonitudes
let leftLongitude = centerCoordinate.longitude - (span.longitudeDelta / 2)
let rightLongitude = centerCoordinate.longitude + (span.longitudeDelta / 2)
let mapSizeInPixels = self.bounds.size
// Get the left and right side of the screen in fully zoomed-in pixels
let leftPixel = self.longitudeToPixelSpaceX(longitude: leftLongitude)
let rightPixel = self.longitudeToPixelSpaceX(longitude: rightLongitude)
let pixelDelta = abs(rightPixel - leftPixel)
let zoomScale = Double(mapSizeInPixels.width) / pixelDelta
let zoomExponent = log2(zoomScale)
let zoomLevel = zoomExponent + 20
return zoomLevel
}
func setCenter(coordinate: CLLocationCoordinate2D, zoomLevel: Int, animated: Bool) {
let zoom = min(zoomLevel, 28)
let span = self.coordinateSpan(centerCoordinate: coordinate, zoomLevel: zoom)
let region = MKCoordinateRegion(center: coordinate, span: span)
self.setRegion(region, animated: true)
}
// MARK: - Private func
private func coordinateSpan(centerCoordinate: CLLocationCoordinate2D, zoomLevel: Int) -> MKCoordinateSpan {
// Convert center coordiate to pixel space
let centerPixelX = self.longitudeToPixelSpaceX(longitude: centerCoordinate.longitude)
let centerPixelY = self.latitudeToPixelSpaceY(latitude: centerCoordinate.latitude)
// Determine the scale value from the zoom level
let zoomExponent = 20 - zoomLevel
let zoomScale = NSDecimalNumber(decimal: pow(2, zoomExponent)).doubleValue
// Scale the map’s size in pixel space
let mapSizeInPixels = self.bounds.size
let scaledMapWidth = Double(mapSizeInPixels.width) * zoomScale
let scaledMapHeight = Double(mapSizeInPixels.height) * zoomScale
// Figure out the position of the top-left pixel
let topLeftPixelX = centerPixelX - (scaledMapWidth / 2)
let topLeftPixelY = centerPixelY - (scaledMapHeight / 2)
// Find delta between left and right longitudes
let minLng: CLLocationDegrees = self.pixelSpaceXToLongitude(pixelX: topLeftPixelX)
let maxLng: CLLocationDegrees = self.pixelSpaceXToLongitude(pixelX: topLeftPixelX + scaledMapWidth)
let longitudeDelta: CLLocationDegrees = maxLng - minLng
// Find delta between top and bottom latitudes
let minLat: CLLocationDegrees = self.pixelSpaceYToLatitude(pixelY: topLeftPixelY)
let maxLat: CLLocationDegrees = self.pixelSpaceYToLatitude(pixelY: topLeftPixelY + scaledMapHeight)
let latitudeDelta: CLLocationDegrees = -1 * (maxLat - minLat)
return MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta)
}
private func longitudeToPixelSpaceX(longitude: Double) -> Double {
return round(MERCATOR_OFFSET + MERCATOR_RADIUS * longitude * M_PI / 180.0)
}
private func latitudeToPixelSpaceY(latitude: Double) -> Double {
if latitude == 90.0 {
return 0
} else if latitude == -90.0 {
return MERCATOR_OFFSET * 2
} else {
return round(MERCATOR_OFFSET - MERCATOR_RADIUS * Double(logf((1 + sinf(Float(latitude * M_PI) / 180.0)) / (1 - sinf(Float(latitude * M_PI) / 180.0))) / 2.0))
}
}
private func pixelSpaceXToLongitude(pixelX: Double) -> Double {
return ((round(pixelX) - MERCATOR_OFFSET) / MERCATOR_RADIUS) * 180.0 / M_PI
}
private func pixelSpaceYToLatitude(pixelY: Double) -> Double {
return (M_PI / 2.0 - 2.0 * atan(exp((round(pixelY) - MERCATOR_OFFSET) / MERCATOR_RADIUS))) * 180.0 / M_PI
}
}
Example of use in your view controller:
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
print("Zoom: \(mapView.getZoomLevel())")
if mapView.getZoomLevel() > 6 {
mapView.setCenter(coordinate: mapView.centerCoordinate, zoomLevel: 6, animated: true)
}
}
Use this example to lock the maximum zoom range, also equally you can limit the minimum
map.cameraZoomRange = MKMapView.CameraZoomRange(maxCenterCoordinateDistance: 1200000)
If you are targeting iOS 13+, use the MKMapView setCameraZoomRange method. Simply provide the min and max center coordinate distances (measured in meters).
See Apple's Documentation here: https://developer.apple.com/documentation/mapkit/mkmapview/3114302-setcamerazoomrange
Don't use regionWillChangeAnimated. Use regionDidChangeAnimated
we can also use setRegion(region, animated: true). Normally it will freeze MKMapView if we use regionWillChangeAnimated, but with regionDidChangeAnimated it works perfectly
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
mapView.checkSpan()
}
extension MKMapView {
func zoom() {
let region = MKCoordinateRegionMakeWithDistance(userLocation.coordinate, 2000, 2000)
setRegion(region, animated: true)
}
func checkSpan() {
let rect = visibleMapRect
let westMapPoint = MKMapPointMake(MKMapRectGetMinX(rect), MKMapRectGetMidY(rect))
let eastMapPoint = MKMapPointMake(MKMapRectGetMaxX(rect), MKMapRectGetMidY(rect))
let distanceInMeter = MKMetersBetweenMapPoints(westMapPoint, eastMapPoint)
if distanceInMeter > 2100 {
zoom()
}
}
}
The MKMapView has, inside of it, a MKScrollView (private API), that is a subclass of UIScrollView. The delegate of this MKScrollView is its own mapView.
So, in order to control the max zoom do the following:
Create a subclass of MKMapView:
MapView.h
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#interface MapView : MKMapView <UIScrollViewDelegate>
#end
MapView.m
#import "MapView.h"
#implementation MapView
-(void)scrollViewDidZoom:(UIScrollView *)scrollView {
UIScrollView * scroll = [[[[self subviews] objectAtIndex:0] subviews] objectAtIndex:0];
if (scroll.zoomScale > 0.09) {
[scroll setZoomScale:0.09 animated:NO];
}
}
#end
Then, access the scroll subview and see the zoomScale property. When the zoom is greater than a number, set your max zoom.
The post by Raphael Petegrosso with the extended MKMapView works great with some small modifications.
The version below is also much more "user friendly", as it gracefully "snaps" back to the defined zoom level as soon as the user lets go of the screen, being similar in feel to Apple's own bouncy scrolling.
Edit: This solution is not optimal and will break/damage the map view, I found a much better solution here: How to detect any tap inside an MKMapView. This allows you to intercept pinching and other motions.
MyMapView.h
#import <MapKit/MapKit.h>
#interface MyMapView : MKMapView <UIScrollViewDelegate>
#end
MyMapView.m
#import "MyMapView.h"
#implementation MyMapView
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale
{
if (scale > 0.001)
{
[scrollView setZoomScale:0.001 animated:YES];
}
}
#end
For a hard limit, use this:
#import "MyMapView.h"
#implementation MyMapView
-(void)scrollViewDidZoom:(UIScrollView *)scrollView
{
if (scrollView.zoomScale > 0.001)
{
[scrollView setZoomScale:0.001 animated:NO];
}
}
#end
The following code worked for me and is conceptually easy to use because it sets the region based on a distance in meters.
The code is derived from the answer posted by: #nevan-king and the comment posted by #Awais-Fayyaz to use regionDidChangeAnimated
Add the following extension to your MapViewDelegate
var currentLocation: CLLocationCoordinate2D?
extension MyMapViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if self.currentLocation != nil, mapView.region.longitudinalMeters > 1000 {
let initialLocation = CLLocation(latitude: (self.currentLocation?.latitude)!,
longitude: (self.currentLocation?.longitude)!)
let coordinateRegion = MKCoordinateRegionMakeWithDistance(initialLocation.coordinate,
regionRadius, regionRadius)
mapView.setRegion(coordinateRegion, animated: true)
}
}
}
Then define an extension for MKCoordinateRegion as follows.
extension MKCoordinateRegion {
/// middle of the south edge
var south: CLLocation {
return CLLocation(latitude: center.latitude - span.latitudeDelta / 2, longitude: center.longitude)
}
/// middle of the north edge
var north: CLLocation {
return CLLocation(latitude: center.latitude + span.latitudeDelta / 2, longitude: center.longitude)
}
/// middle of the east edge
var east: CLLocation {
return CLLocation(latitude: center.latitude, longitude: center.longitude + span.longitudeDelta / 2)
}
/// middle of the west edge
var west: CLLocation {
return CLLocation(latitude: center.latitude, longitude: center.longitude - span.longitudeDelta / 2)
}
/// distance between south and north in meters. Reverse function for MKCoordinateRegionMakeWithDistance
var latitudinalMeters: CLLocationDistance {
return south.distance(from: north)
}
/// distance between east and west in meters. Reverse function for MKCoordinateRegionMakeWithDistance
var longitudinalMeters: CLLocationDistance {
return east.distance(from: west)
}
}
The above snippet for MKCoordinateRegion was posted by #Gerd-Castan on this question:
Reverse function of MKCoordinateRegionMakeWithDistance?
I've run into this very issue at work and have created something that works fairly well without setting a global limit.
The MapView delegates that I leverage are:
- mapViewDidFinishRendering
- mapViewRegionDidChange
The premise behind my solution is that since a satellite view renders an area with no data it is always the same thing. This dreaded image (http://imgur.com/cm4ou5g) If we can comfortably rely on that fail case we can use it as a key for determining wha the user is seeing. After the map renders, I take a screenshot of the rendered map bounds and determing an average RGB value. Based off of that RGB value, I assume that the area in question has no data. If that's the case I pop the map back out to the last span that was rendered correctly.
The only global check I have is when it starts to check the map, you can increase or decrease that setting based on your needs. Below is the raw code that will accomplish this and will be putting together a sample project for contribution. Any optimizations you can offer would be appreciated and hope it helps.
#property (assign, nonatomic) BOOL isMaxed;
#property (assign, nonatomic) MKCoordinateSpan lastDelta;
self.lastDelta = MKCoordinateSpanMake(0.006, 0.006);
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
if (mapView.mapType != MKMapTypeStandard && self.isMaxed) {
[self checkRegionWithDelta:self.lastDelta.longitudeDelta];
}
}
- (void)checkRegionWithDelta:(float)delta {
if (self.mapView.region.span.longitudeDelta < delta) {
MKCoordinateRegion region = self.mapView.region;
region.span = self.lastDelta;
[self.mapView setRegion:region animated:NO];
} else if (self.mapView.region.span.longitudeDelta > delta) {
self.isMaxed = NO;
}
}
- (void)mapViewDidFinishRenderingMap:(MKMapView *)mapView fullyRendered:(BOOL)fullyRendered {
if (mapView.mapType != MKMapTypeStandard && !self.isMaxed) {
[self checkToProcess:self.lastDelta.longitudeDelta];
}
}
- (void)checkToProcess:(float)delta {
if (self.mapView.region.span.longitudeDelta < delta) {
UIGraphicsBeginImageContext(self.mapView.bounds.size);
[self.mapView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *mapImage = UIGraphicsGetImageFromCurrentImageContext();
[self processImage:mapImage];
}
}
- (void)processImage:(UIImage *)image {
self.mapColor = [self averageColor:image];
const CGFloat* colors = CGColorGetComponents( self.mapColor.CGColor );
[self handleColorCorrection:colors[0]];
}
- (void)handleColorCorrection:(float)redColor {
if (redColor < 0.29) {
self.isMaxed = YES;
[self.mapView setRegion:MKCoordinateRegionMake(self.mapView.centerCoordinate, self.lastDelta) animated:YES];
} else {
self.lastDelta = self.mapView.region.span;
}
}
- (UIColor *)averageColor:(UIImage *)image {
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
unsigned char rgba[4];
CGContextRef context = CGBitmapContextCreate(rgba, 1, 1, 8, 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), image.CGImage);
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
if(rgba[3] > 0) {
CGFloat alpha = ((CGFloat)rgba[3])/255.0;
CGFloat multiplier = alpha/255.0;
return [UIColor colorWithRed:((CGFloat)rgba[0])*multiplier
green:((CGFloat)rgba[1])*multiplier
blue:((CGFloat)rgba[2])*multiplier
alpha:alpha];
}
else {
return [UIColor colorWithRed:((CGFloat)rgba[0])/255.0
green:((CGFloat)rgba[1])/255.0
blue:((CGFloat)rgba[2])/255.0
alpha:((CGFloat)rgba[3])/255.0];
}
}

Refresh MKAnnotationView on a MKMapView

I'd like to a-synchronically load images for my custom MKAnnotationView; I'm already using the EGOImageView-framework (it goes very well with UITableViews), but I fail to make it work on MKMapView. Images seem to be loaded, but I cannot refresh them on the map - [myMap setNeedsDisplay] does nothing.
I haven't tried it myself, but it might work:
Get a view for annotation using MKMapView - (MKAnnotationView *)viewForAnnotation:(id <MKAnnotation>)annotation method and set new image to it.
Edit: Tried to do that myself and this approach worked fine for me:
// Function where you got new image and want to set it to annotation view
for (MyAnnotationType* myAnnot in myAnnotationContainer){
MKAnnotationView* aView = [mapView viewForAnnotation: myAnnot];
aView.image = [UIImage imageNamed:#"myJustDownloadedImage.png"];
}
After calling this method all annotations images were updated.
I found only one reliable way to update annotation, others methods didn't work for me: just removing and adding annotation again:
id <MKAnnotation> annotation;
// ...
[mapView removeAnnotation:annotation];
[mapView addAnnotation:annotation];
This code will make -[mapView:viewForAnnotation:] to be called again and so your annotation view will be created again (or maybe reused if you use dequeueReusableAnnotationViewWithIdentifier) with correct data.
This is the only way I've found to force redraw of MKAnnotationViews:
// force update of map view (otherwise annotation views won't redraw)
CLLocationCoordinate2D center = mapView.centerCoordinate;
mapView.centerCoordinate = center;
Seems useless, but it works.
This worked for me
#import "EGOImageView.h"
- (MKAnnotationView *)mapView:(MKMapView *)theMapView viewForAnnotation:(id <MKAnnotation>)annotation
{
//ENTER_METHOD;
if([annotation isKindOfClass:[MKUserLocation class]]) return nil;
MKPinAnnotationView *annView;
static NSString *reuseIdentifier = #"reusedAnnView";
annView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
annView.canShowCallout = YES;
annView.calloutOffset = CGPointMake(-5.0f, 0.0f);
annView.opaque = NO;
annView.image = [[UIImage imageNamed:#"tempImage.png"] retain];
YourModel *p = annotation; // make this conform to <MKAnnotation>
EGOImageView *egoIV = [[EGOImageView alloc] initWithPlaceholderImage:[UIImage imageNamed:#"tempImage.png"]];
[egoIV setDelegate:self];
egoIV.imageURL = [NSURL URLWithString:p.theUrlToDownloadFrom];
[annView addSubview:egoIV];
[egoIV release];
return [annView autorelease];
}
Thank you #Vladimir for the help! Your answer inspired my answer.
I made some changes to make it clearer.
func addAnnotation(coordinate: CLLocationCoordinate2D) {
let annotation = Annotation()
annotation.coordinate = coordinate
annotations.append(annotation)
mapView.addAnnotation(annotation)
for index in 0..<annotations.count {
if let view = mapView.view(for: annotations[index]) as? AnnotationView {
if index == 0 {
view.imageView.image = NSImage(named: "icon_positioning")
} else {
view.imageView.image = NSImage(named: "icon_positioning")
}
}
}
}
Also the protocol in MKMapview needs to implement:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let view = mapView.dequeueReusable(withClass: AnnotationView.self, annotation: annotation)
if index == 0 {
view.imageView.image = NSImage(named: "icon_positioning")
} else {
view.imageView.image = NSImage(named: "icon_positioning")
}
return view
}
By the way, the AnnotationView initialization here is:
public extension MKMapView {
func dequeueReusable<T: MKAnnotationView>(withClass name: T.Type,annotation: MKAnnotation?) -> T {
if let view = dequeueReusableAnnotationView(withIdentifier: T.identifier()) as? T {
return view
}
let view = T.init(annotation: annotation, reuseIdentifier: T.identifier())
return view
}
}
Thanks again #Vladimir for the answer
(I don't see how to indicate that this post pertains to zerodiv's answer.) I was able to get zerodiv's method to work by adding a kickstart of sorts, forcing a meaningless but different coordinate, then restoring the prior, correct one. The screen doesn't flash at all because of the first location.
CLLocationCoordinate2D center = mapView.centerCoordinate;
CLLocationCoordinate2D forceChg;
forceChg.latitude = 0;
forceChg.longitude = curLongNum;
mapView.centerCoordinate = forceChg;
mapView.centerCoordinate = center;
I wanted to refresh user location pin with new selected avatar. I found another fast solution for that case. I have 2 screens. The first one is for selecting avatar and the second is for displaying user on map.
swift 5
You can change user location image by creating MKAnnotationView
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
let userIdentifier = "UserLocation"
var userView = mapView.dequeueReusableAnnotationView(withIdentifier: userIdentifier)
if userView == nil {
userView = MKAnnotationView(annotation: annotation, reuseIdentifier: userIdentifier)
} else {
userView?.annotation = annotation
}
userView!.image = UIImage(named: "avatar1") //your image
return userView
}
return nil
}
So if user will change avatar to something else, for example to "avatar2" it won't refresh.
In order to refresh user location image i added this code in viewWillAppear()
self.mapView.showsUserLocation = false
self.mapView.showsUserLocation = true

iPhone - Get Position of UIView within entire UIWindow

The position of a UIView can obviously be determined by view.center or view.frame etc. but this only returns the position of the UIView in relation to it's immediate superview.
I need to determine the position of the UIView in the entire 320x480 co-ordinate system. For example, if the UIView is in a UITableViewCell it's position within the window could change dramatically irregardless of the superview.
Any ideas if and how this is possible?
That's an easy one:
[aView convertPoint:localPosition toView:nil];
... converts a point in local coordinate space to window coordinates. You can use this method to calculate a view's origin in window space like this:
[aView.superview convertPoint:aView.frame.origin toView:nil];
2014 Edit: Looking at the popularity of Matt__C's comment it seems reasonable to point out that the coordinates...
don't change when rotating the device.
always have their origin in the top left corner of the unrotated screen.
are window coordinates: The coordinate system ist defined by the bounds of the window. The screen's and device coordinate systems are different and should not be mixed up with window coordinates.
Swift 5+:
let globalPoint = aView.superview?.convert(aView.frame.origin, to: nil)
Swift 3, with extension:
extension UIView{
var globalPoint :CGPoint? {
return self.superview?.convert(self.frame.origin, to: nil)
}
var globalFrame :CGRect? {
return self.superview?.convert(self.frame, to: nil)
}
}
In Swift:
let globalPoint = aView.superview?.convertPoint(aView.frame.origin, toView: nil)
Here is a combination of the answer by #Mohsenasm and a comment from #Ghigo adopted to Swift
extension UIView {
var globalFrame: CGRect? {
let rootView = UIApplication.shared.keyWindow?.rootViewController?.view
return self.superview?.convert(self.frame, to: rootView)
}
}
For me this code worked best:
private func getCoordinate(_ view: UIView) -> CGPoint {
var x = view.frame.origin.x
var y = view.frame.origin.y
var oldView = view
while let superView = oldView.superview {
x += superView.frame.origin.x
y += superView.frame.origin.y
if superView.next is UIViewController {
break //superView is the rootView of a UIViewController
}
oldView = superView
}
return CGPoint(x: x, y: y)
}
Works well for me :)
extension UIView {
var globalFrame: CGRect {
return convert(bounds, to: window)
}
}
this worked for me
view.layoutIfNeeded() // this might be necessary depending on when you need to get the frame
guard let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else { return }
let frame = yourView.convert(yourView.bounds, to: keyWindow)
print("frame: ", frame)