How to keep pin and map centered above moving overlay in an MKMapView
Asked Answered
F

2

8

How can I keep a pin centered on a map whilst I move (via Pan Gesture) another view vertically over the map such that the pin remains above the overlay (not an actual MapKit overlay).

See attached screenshots for the first and final states.

I've got the CGRect of the space between the overlay and the top of the screen as the user pan's up / down. However, how I use that to move the map and pin whilst zooming into the map as the user pans upward..and zoom back out again when the user pans downward, has eluded me so far.

I've tried different approaches, from attempting to adjust the visible rect to adjusting the map view's frame. The answer may lie in some MKMapRect / Region trickery..

Initial state

Final state with overlay panned upward

(Hand icon by Freepik CC BY 3.0)

Fretwell answered 17/5, 2015 at 14:49 Comment(3)
have you tried setCenterCoordinate approach?Ninety
Is the map supposed to be zooming in and out when you pan the overlay?Samal
@Samal Yes, inward as you pan upward and outward as you pan downward..Fretwell
V
6

Actually, keithbhunter's code is slow because besides updating the region faster than it can load it, the map is also changing height which causes extra overhead!

I updated the code so that it runs smooth.

With this code what i do is keep the map view the same size but instead i move the point of center to compensate for the height of the sliding view.

For this code to work, you have to modify keithbhunter's setup so that the mapView's bottom constraint is pinned completely to the superview's bottom (and not to the slidingView (so that the mapView is always the same size as the super view). For the rest the setup is the same.

enter image description here

Also it is possible to customize the amount of zoom with the variable maxMetersDistance

Here i'm, always centering on the Eifel tower

enter image description here enter image description here

import UIKit
import MapKit

class ViewController: UIViewController {

    @IBOutlet weak var mapView: MKMapView!
    @IBOutlet weak var slidingView: UIView!
    @IBOutlet weak var slidingViewHeight: NSLayoutConstraint!
    var maxMetersDistance:CGFloat = 10000.0; // customize this to set how far the map zooms out of the interest area

    override func viewDidLoad() {
        super.viewDidLoad()
        let pan = UIPanGestureRecognizer(target: self, action: "viewDidPan:")
        self.slidingView.addGestureRecognizer(pan)
        firstTimeCenter()
    }

    func firstTimeCenter(){
         var coordinate = CLLocationCoordinate2D(latitude: 48.8582, longitude: 2.2945)
        let region = MKCoordinateRegionMakeWithDistance(coordinate, Double(maxMetersDistance), Double(maxMetersDistance))
        self.mapView.setRegion(region, animated: true)
    }

    func reloadMap() {
        let height = CGFloat(self.slidingViewHeight.constant)
        var regionDistance = (maxMetersDistance / self.view.frame.height) * height
        regionDistance = maxMetersDistance - regionDistance
        var coordinate = CLLocationCoordinate2D(latitude: 48.8582, longitude: 2.2945)
        var mapRect = mapView.visibleMapRect;
        var metersPerMapPoint = MKMetersPerMapPointAtLatitude(coordinate.latitude);
        var metersPerPixel = CGFloat(metersPerMapPoint) * CGFloat(mapRect.size.width) / CGFloat(mapView.bounds.size.width);
        var totalMeters = Double(metersPerPixel) * Double(height/2)

        coordinate = self.translateCoord(coordinate, MetersLat: -totalMeters, MetersLong: 0.0)

        let region = MKCoordinateRegionMakeWithDistance(coordinate, Double(regionDistance), Double(regionDistance))
        self.mapView.setRegion(region, animated: false)
    }

    func viewDidPan(panGesture: UIPanGestureRecognizer) {
        let location = panGesture.locationInView(self.view)
        self.slidingViewHeight.constant = self.view.frame.size.height - location.y
        self.reloadMap()
    }

    func translateCoord(coord:CLLocationCoordinate2D, MetersLat:Double,  MetersLong:Double)->CLLocationCoordinate2D{
        var tempCoord = CLLocationCoordinate2D()
        var tempRegion = MKCoordinateRegionMakeWithDistance(coord, MetersLat, MetersLong);
        var tempSpan = tempRegion.span;
        tempCoord.latitude = coord.latitude + tempSpan.latitudeDelta;
        tempCoord.longitude = coord.longitude + tempSpan.longitudeDelta;
        return tempCoord;
    }

}
Vancouver answered 19/6, 2015 at 13:50 Comment(1)
Thanks Juan & Keith! There is some jumpiness in Juan's solution which I have to figure out but this is very close to what I was looking for :)Fretwell
S
4
class ViewController: UIViewController {

    @IBOutlet weak var mapView: MKMapView!
    @IBOutlet weak var slidingView: UIView!
    @IBOutlet weak var slidingViewHeight: NSLayoutConstraint!


    override func viewDidLoad() {
        super.viewDidLoad()
        self.reloadMap()
        let pan = UIPanGestureRecognizer(target: self, action: "viewDidPan:")
        self.slidingView.addGestureRecognizer(pan)
    }

    func reloadMap() {
        let coordinate = CLLocationCoordinate2D(latitude: 37.332363, longitude: -122.030805)
        let height = Double(self.mapView.frame.size.height)
        let regionDistance = 0.3 * height * height  // random multiplier and exponential equation for scaling
        let region = MKCoordinateRegionMakeWithDistance(coordinate, regionDistance, regionDistance)
        self.mapView.setRegion(region, animated: false)
    }

    func viewDidPan(panGesture: UIPanGestureRecognizer) {
        let location = panGesture.locationInView(self.view)
        self.slidingViewHeight.constant = self.view.frame.size.height - location.y
        self.reloadMap()
    }

}

To setup the views, place a map view and a UIView in your view controller. Use autolayout to pin the map view's left, top and right sides to the superview. Then pin the bottom of the map view to the top of the UIView. Next, pin the left, bottom and right sides of the UIView to the superview. Finally, set a height constraint on the UIView to whatever you want it to initialize to. This height value will be changed as the user drags the view. This allows the UIView to grow as we please and appease autolayout at the same time.

Add an @IBOutlet to your view controller for the map view, the UIView and the UIView's height constraint, like in the code above. The regionDistance property is what is doing the zooming magic here. It is an exponential equation (that I made up randomly) which will calculate the region for either larger or smaller based on the map view's height. reloadMap() uses this to update the map's zoom. Tying it all together is the UIPanGestureRecognizer on the UIView, which is what controls the UIView's height, causing the zooming action on the map.

Pitfall: This forces the map to update the region faster than it can load the region, making it look very jumpy. There is probably ways around this. Get creative.

The coordinate I used in the example is Apple's headquarters.

Apple's HQ

Samal answered 19/6, 2015 at 0:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.