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