Sample - San Diego Zoo App

2 minute read

This app displays a sample format for a map-oriented tab bar application. It includes a tab bar controller with home, map, and search tabs. The banner image of this page is the splash screen of the app and was created in Photoshop.

This sample includes the following features:

Map Tab

  • MKMapView with location tracking
  • Custom UIGestureRecognizer to prevent user from zooming in or out too much
  • Method to adjust mapView region if user tries to move outside zoo bounds
  • MKOverlay rendering (tiles, MKPolylines, custom MKAnnotationViews for each type of zoo object)
  • UIPickerView to filter map points by area
  • DropDown table to filter map points by type

Home Tab

  • Custom Experience class and subclasses separated by headers in tableView
  • Browser url link for ticket purchase
  • App-specific social media url links for Facebook, Instagram, Twitter, and Youtube

Search Tab

  • UIPickerView to filter search results by area
  • UICollectionView to filter search results by type
  • UISearchController to filter tableView data by search text

Misc

  • Custom UITabBarController with button to navigate to detached MapViewController to prevent memory allocation buildup with MKMapView object
  • UITableViewCell subclass with Programmatic UI
  • ObjectDetailViewController with Programmatic UI

Screenshots

linearly separable data

Sample Code:

Link to the git repository: click here

GestureRecognizer subclass:

import UIKit.UIGestureRecognizerSubclass

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

ZoomInterceptor Protocol:

import UIKit
import MapKit

// Set Zoom Gesture Recognizer for controllers' mapView

protocol MapViewZoomInterceptor {
    var zoomInterceptor: WildCardGestureRecognizer! { get set }
    
    func setMapViewZoomInterceptor(mapView: MKMapView)
}

extension MapViewZoomInterceptor where Self: UIViewController {
    func setMapViewZoomInterceptor(mapView: MKMapView) {
        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
        }
        
        // started zooming
        zoomInterceptor.touchesBeganCallback = {_, _ in
            mapView.isZoomEnabled = true
        }
        
        // currently zooming
        zoomInterceptor.touchesMovedCallback = {_, _ in
            // if zooming out
            if self.zoomInterceptor.scale < 1 {
                let positionChanged = (latitude != mapView.region.center.latitude || longitude != mapView.region.center.longitude)
                let latTooBig = (mapView.region.span.latitudeDelta > (northernBorder - southernBorder))
                let longTooBig = (mapView.region.span.longitudeDelta > (easternBorder - westernBorder))
                
                // don't change zoom if don't need to
                guard positionChanged || latTooBig || longTooBig else { return }
                
                let maxSpan = MKCoordinateSpan.init(latitudeDelta: 0.007, longitudeDelta: 0.007)
                let latDeltaTooBig = mapView.region.span.latitudeDelta > maxSpan.latitudeDelta
                let longDeltaTooBig = mapView.region.span.longitudeDelta > maxSpan.longitudeDelta
                
                if latDeltaTooBig || longDeltaTooBig {
                    mapView.isZoomEnabled = false
                } else {
                    mapView.isZoomEnabled = true
                }
                // if zooming in
            } else if self.zoomInterceptor.scale > 1 {
                let minimumSpan = MKCoordinateSpan.init(latitudeDelta: 0.002, longitudeDelta: 0.002)
                let latDeltaTooSmall = mapView.region.span.latitudeDelta < minimumSpan.latitudeDelta
                let longDeltaTooSmall = mapView.region.span.longitudeDelta < minimumSpan.longitudeDelta
                
                if latDeltaTooSmall || longDeltaTooSmall {
                    mapView.isZoomEnabled = false
                } else {
                    mapView.isZoomEnabled = true
                }
            }
        }
        
        // done zooming
        zoomInterceptor.touchesEndedCallback = {_, _ in
            mapView.isZoomEnabled = true
        }
        
        mapView.addGestureRecognizer(zoomInterceptor)
    }
}