iosswiftuikitmapkituipinchgesturerecognizer

Unable to deallocate Gesture Recognizer from Memory Stack


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
    }
}

Solution

  • 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