0

I'm trying to implement a Custom Gesture Recognizer on my mapView that will prevent users from zooming in or out past a certain threshold set by a MKCoordinateSpan.

The mapView's ViewController is part of a tab bar Controller, so I'm removing the mapView each time the view disappears and re-adding it for memory purposes.

Since I've added the Custom Gesture Recognizer, the memory isn't being deallocated when the view disappears. What am I missing besides removing the gesture recognizer from the mapView?

MapViewController:

class MapViewController: UIViewController, CLLocationManagerDelegate {

    @IBOutlet var mapView: MKMapView!

    override func viewDidLoad() {
        super.viewDidLoad()
        loadMapView()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        if mapView == nil {
            loadMapView()
        }
    }

    override func viewDidDisappear(_ animated:Bool) {
        super.viewDidDisappear(animated)
        self.applyMapViewMemoryFix()
    }

    func loadMapView() {
        self.edgesForExtendedLayout = []
        setMapView()
    }

    func setMapView() {
        if self.mapView == nil {
            addMapView()
        }

        mapView.delegate = self
        mapView.mapType = .mutedStandard
        mapView.autoresizingMask = [.flexibleWidth,.flexibleHeight]
    }

    func addMapView() {
        mapView = MKMapView()
        mapView.frame = self.navigationController!.view.bounds
        mapView.mapType = MKMapType.standard
        mapView.isZoomEnabled = true
        mapView.isScrollEnabled = true
        self.view.addSubview(mapView)
    }

    func applyMapViewMemoryFix() {
        for recognizer in (self.mapView?.gestureRecognizers)! {
            if recognizer is WildCardGestureRecognizer {
                self.mapView.removeGestureRecognizer(recognizer)
            }
        }

        self.mapView.showsUserLocation = false
        self.mapView.delegate = nil
        self.mapView.removeFromSuperview()
        self.mapView = nil
    }

}

Extension where I set up boundaries for gesture recognizer:

extension MapViewController: MKMapViewDelegate {

    // View Region Changing
    func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
        let northernBorder = 32.741152
        let southernBorder = 32.731461
        let easternBorder = -117.143622
        let westernBorder = -117.157399

        var latitude  = mapView.region.center.latitude
        var longitude = mapView.region.center.longitude

        if (mapView.region.center.latitude > northernBorder) {
            latitude = northernBorder
        }

        if (mapView.region.center.latitude <  southernBorder) {
            latitude = southernBorder
        }

        if (mapView.region.center.longitude > easternBorder) {
            longitude = easternBorder
        }

        if (mapView.region.center.longitude < westernBorder) {
            longitude = westernBorder
        }

        let tapInterceptor = WildCardGestureRecognizer(target: nil, action: nil)

        tapInterceptor.touchesBeganCallback = {_, _ in
            mapView.isZoomEnabled = true
        }

        tapInterceptor.touchesMovedCallback = {_, _ in
            if tapInterceptor.scale < 1 {
                if (latitude != mapView.region.center.latitude || longitude != mapView.region.center.longitude)
                    || ((mapView.region.span.latitudeDelta > (northernBorder - southernBorder) )
                        || (mapView.region.span.longitudeDelta > (easternBorder - westernBorder))) {
                    let span = MKCoordinateSpan.init(latitudeDelta: 0.007, longitudeDelta: 0.007)
                    if mapView.region.span.latitudeDelta > span.latitudeDelta || mapView.region.span.longitudeDelta > span.longitudeDelta {
                        mapView.isZoomEnabled = false
                    } else {
                        mapView.isZoomEnabled = true
                    }
                }
            } else if tapInterceptor.scale > 1 {
                let minimumSpan = MKCoordinateSpan.init(latitudeDelta: 0.002, longitudeDelta: 0.002)
                if mapView.region.span.latitudeDelta < minimumSpan.latitudeDelta || mapView.region.span.longitudeDelta < minimumSpan.longitudeDelta {
                    mapView.isZoomEnabled = false
                } else {
                    mapView.isZoomEnabled = true
                }
            }
        }

        tapInterceptor.touchesEndedCallback = {_, _ in
            mapView.isZoomEnabled = true
        }

        mapView.addGestureRecognizer(tapInterceptor)
    }
}

The Custom Gesture Recognizer:

class WildCardGestureRecognizer: UIPinchGestureRecognizer {

    var touchesBeganCallback: ((Set<UITouch>, UIEvent) -> Void)?
    var touchesMovedCallback: ((Set<UITouch>, UIEvent) -> Void)?
    var touchesEndedCallback: ((Set<UITouch>, UIEvent) -> Void)?

    override init(target: Any?, action: Selector?) {
        super.init(target: target, action: action)
        self.cancelsTouchesInView = false
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesBegan(touches, with: event)
        touchesBeganCallback?(touches, event)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesMoved(touches, with: event)
        touchesMovedCallback?(touches, event)
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesEnded(touches, with: event)
        touchesEndedCallback?(touches, event)
    }

    override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool {
        return false
    }

    override func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool {
        return false
    }
}
Wilson Desimini
  • 722
  • 5
  • 10

1 Answers1

2

The memory leaks as you declare the gesture here

 let tapInterceptor = WildCardGestureRecognizer(target: nil, action: nil)
 .
 .
 .
 mapView.addGestureRecognizer(tapInterceptor)

inside regionDidChangeAnimated , since it's called mutiple times you'll get many gestures added to the mapview as the region changed , so it's better to create an instance var like

var tapInterceptor:WildCardGestureRecognizer!

and add the gesture init and callbacks inside a function then call it form viewDidLoad

Also remove @IBOutle

@IBOutlet var mapView: MKMapView!

If you don't make it inside storyboard , Also i don't think remove/add way will make difference as deallocation of objects in IOS always doesn't release the whole taken part , so it's better to leave the mapview with it's only 1 gesture instead of accumulating a big leak from the ones you loss as you select/deselect that tap

Shehata Gamal
  • 98,760
  • 8
  • 65
  • 87
  • Thank you for the quick advice in getting rid of that leak, totally worked, but I'm a little confused on the follow up advice of leaving mapView with it's 1 gesture. Are you saying don't continually add and remove the mapView? Because the MapViewController is part of a UITabBarController, moving between tabs doesn't allocate the memory. So the only way I've found to deallocate it is to remove the mapView. – Wilson Desimini Dec 03 '18 at 23:25
  • 1
    As i said above this won't totally release it , and to measure that create an empty vc , with a button action add 1000 gestures to a view , and with the other clear them , you will see drop in memory , but it will never be the same as before adding same applies for adding/removing the map – Shehata Gamal Dec 03 '18 at 23:28
  • 1
    Didn't say remove/add is wrong but with time it'll leak memory more than leaving it , it depends on user interaction on how fastly he switches the taps , you re free in this point – Shehata Gamal Dec 03 '18 at 23:30
  • I get it, I've noticed in instruments that with each removal. It still slightly increases with each removal compared to when it was first added. But if I just leave it, won't the memory just keep building over time until it crashes? Do you suggest anything else in terms of deallocating memory? – Wilson Desimini Dec 04 '18 at 16:00