tvOSMaps/MapViewController.swift
/*  | 
Copyright (C) 2016 Apple Inc. All Rights Reserved.  | 
See LICENSE.txt for this sample’s licensing information  | 
Abstract:  | 
A viw controller that displays an array of `SearchableItem`s on an `MKMapView` and in `UITableView`.  | 
*/  | 
import UIKit  | 
import MapKit  | 
class MapViewController: UIViewController { | 
static let tableViewCellIdentifier = "SearchResultCell"  | 
// MARK: Interface builder outlets  | 
@IBOutlet var mapView: MKMapView!  | 
@IBOutlet var tableView: UITableView!  | 
@IBOutlet var tableViewTrailingConstraint: NSLayoutConstraint!  | 
// MARK: Properties  | 
/// Array of items to show on the map.  | 
var items = [SearchableItem]()  | 
/// The selected item currently selected by the user.  | 
var selectedItem: SearchableItem?  | 
    var highlightedItem: SearchableItem? { | 
        didSet { | 
            guard oldValue != highlightedItem else { return } | 
            if let oldValue = oldValue { | 
reloadAnnotation(for: oldValue)  | 
}  | 
            if let newValue = highlightedItem { | 
reloadAnnotation(for: newValue)  | 
}  | 
}  | 
}  | 
/// Gesture recognizer to handle re-displaying the table view.  | 
private var menuGestureRecognizer: UITapGestureRecognizer?  | 
/// Gesture recognizer to detect selecting when an annotation has focos  | 
fileprivate var selectGestureRecognizer: UITapGestureRecognizer?  | 
/// The hidden state of the table view.  | 
    fileprivate var tableViewHidden = false { | 
        didSet { | 
// Check if the value has changed and the view has loaded.  | 
            guard isViewLoaded && tableViewHidden != oldValue else { return } | 
/*  | 
Update the constraint to position the table view on or off the  | 
screen and mark the view as needing to be laid out.  | 
*/  | 
tableViewTrailingConstraint.constant = tableViewHidden ? -tableViewOverlapWidth : 0  | 
view.layoutIfNeeded()  | 
/*  | 
Enable the gesture recognizer to detect the menu button being  | 
pressed if the table view is hidden. Pressing the menu button  | 
in this state should re-show the table view.  | 
*/  | 
menuGestureRecognizer?.isEnabled = tableViewHidden  | 
selectGestureRecognizer?.isEnabled = tableViewHidden  | 
}  | 
}  | 
    private var tableViewOverlapWidth: CGFloat { | 
return tableView.superview!.bounds.size.width  | 
}  | 
    override var preferredFocusEnvironments: [UIFocusEnvironment] { | 
        get { | 
/*  | 
The focus should default to the selected table view cell if the  | 
table view is visible.  | 
*/  | 
            if !tableViewHidden { | 
                if let indexPath = tableView.indexPathForSelectedRow, let cell = tableView.cellForRow(at: indexPath) { | 
return [cell]  | 
}  | 
                else { | 
return [tableView]  | 
}  | 
}  | 
// Fall back to the default preferred focused view.  | 
return super.preferredFocusEnvironments  | 
}  | 
}  | 
// MARK: UIViewController  | 
    override func viewDidLoad() { | 
super.viewDidLoad()  | 
view.layoutIfNeeded()  | 
        guard let selectedItem = selectedItem else { fatalError("No item selected") } | 
// Set the table view's initial state to hidden.  | 
tableViewHidden = true  | 
/*  | 
Create a gesture recognizer to detect the menu button being pressed  | 
and add it to the map view. This will be used to re-show the table  | 
view if it's hidden.  | 
*/  | 
menuGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleMenuGestureRecognizer(_:)))  | 
menuGestureRecognizer?.allowedPressTypes = [NSNumber(integerLiteral: UIPressType.menu.rawValue)]  | 
mapView.addGestureRecognizer(menuGestureRecognizer!)  | 
/*  | 
Create a gesture recogniser to detect the selecte button being pressed  | 
and add it to the map view. This will be used to detect when the  | 
user clicks a selected annotation.  | 
*/  | 
selectGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleSelectGestureRecognizer(_:)))  | 
selectGestureRecognizer?.allowedPressTypes = [NSNumber(integerLiteral: UIPressType.select.rawValue)]  | 
selectGestureRecognizer?.delegate = self  | 
mapView.addGestureRecognizer(selectGestureRecognizer!)  | 
// Populate the map view with annotations for each item.  | 
        let newAnnotations: [MKAnnotation] = items.map { SearchResultMapAnnotation(item: $0) } | 
mapView.showAnnotations(newAnnotations, animated: false)  | 
// Select the annotation for the currently selected item.  | 
let annotation = self.annotation(for: selectedItem)  | 
mapView.selectAnnotation(annotation, animated: false)  | 
// Select the table view cell for the currently selected item.  | 
        if let row = items.index(of: selectedItem) { | 
let indexPath = IndexPath(row: row, section: 0)  | 
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .middle)  | 
}  | 
}  | 
// MARK: UIFocusEnvironment  | 
    override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { | 
        guard let nextFocusedView = context.nextFocusedView, let previouslyFocusedView = context.previouslyFocusedView else { return } | 
/*  | 
If the focus has moved from the table view to the map view, hide the  | 
table view.  | 
*/  | 
        if nextFocusedView.isDescendant(of: mapView) && previouslyFocusedView.isDescendant(of: tableView) { | 
animateTableView(hidden: true)  | 
// Select the annotation for the currently selected item.  | 
            if let selectedItem = selectedItem { | 
let annotation = self.annotation(for: selectedItem)  | 
mapView.selectAnnotation(annotation, animated: false)  | 
}  | 
}  | 
}  | 
// MARK: Gesture recognizer handlers  | 
    func handleMenuGestureRecognizer(_ recognizer: UITapGestureRecognizer) { | 
// Hide the table view if the menu button has been tapped.  | 
        if recognizer.state == .ended { | 
animateTableView(hidden: false)  | 
}  | 
}  | 
    func handleSelectGestureRecognizer(_ recognizer: UITapGestureRecognizer) { | 
// If the recognizer state is `Ended`, the user selected an annotation.  | 
        if let selectedItem = selectedItem, recognizer.state == .ended { | 
            print("Selected \(selectedItem.title)") | 
}  | 
}  | 
// MARK: Convenience  | 
/// Animates the table view, ensuring the correct selection state for map annotations.  | 
    fileprivate func animateTableView(hidden: Bool) { | 
// If the requested state is the same as the current state, do nothing.  | 
        guard tableViewHidden != hidden else { return } | 
        guard let selectedItem = selectedItem else { fatalError("Mo item selected") } | 
/*  | 
Determine an appropriate animation curve to used depending on  | 
whether the table view is being shown or hidden.  | 
*/  | 
let animationCurve: UIViewAnimationOptions = hidden ? .curveEaseIn : .curveEaseOut  | 
let selectedItemAnnotation = annotation(for: selectedItem)  | 
        if hidden { | 
// Select the annotation for the selected item.  | 
highlightedItem = nil  | 
mapView.selectAnnotation(selectedItemAnnotation, animated: true)  | 
}  | 
        else { | 
/*  | 
Prevent the focus engine selecting an annotation during the  | 
animation.  | 
*/  | 
setAnnotationSelectionEnabled(false)  | 
// De-select the annotation for the selected item.  | 
mapView.deselectAnnotation(selectedItemAnnotation, animated: true)  | 
}  | 
// Wrap a call to set the hidden state in an `UIView` animation block.  | 
        UIView.animate(withDuration: 0.25, delay: 0, options: [animationCurve], animations: { | 
self.tableViewHidden = hidden  | 
        }, completion: { _ in | 
// Trigger a focus update.  | 
self.setNeedsFocusUpdate()  | 
self.updateFocusIfNeeded()  | 
// Re-enable the annotation views.  | 
self.setAnnotationSelectionEnabled(true)  | 
})  | 
// If the table view has been show, make sure all the annotations are visible.  | 
        if !hidden { | 
mapView.layoutMargins.right = tableViewOverlapWidth  | 
mapView.showAnnotations(mapView.annotations, animated: true)  | 
}  | 
        else { | 
mapView.layoutMargins.right = mapView.layoutMargins.left  | 
}  | 
}  | 
/// Returns the `SearchResultMapAnnotation` instance that represents the passed `SearchableItem`.  | 
    private func annotation(for item: SearchableItem) -> SearchResultMapAnnotation { | 
        let foundAnnotation = mapView.annotations.flatMap { annotation in | 
return annotation as? SearchResultMapAnnotation  | 
        }.filter { annotation in | 
return annotation.item == item  | 
}.first  | 
        guard let annotation = foundAnnotation else { fatalError("Unable to find annotation for item") } | 
return annotation  | 
}  | 
    private func reloadAnnotation(for item: SearchableItem) { | 
let annotation = self.annotation(for: item)  | 
mapView.removeAnnotation(annotation)  | 
mapView.addAnnotation(annotation)  | 
}  | 
/**  | 
Sets the enabled state of all annotation views on the map.  | 
If annotation views are enabled they can become focused when the map is  | 
in the annotation selection mode and focus moves from the map to the  | 
table view or vice verca.  | 
*/  | 
    private func setAnnotationSelectionEnabled(_ enabled: Bool) { | 
/*  | 
Set the map view'd delegate depending on whether we want notifications  | 
of changes to selection.  | 
*/  | 
mapView.delegate = enabled ? self : nil  | 
        for annotation in mapView.annotations { | 
mapView.view(for: annotation)?.isEnabled = enabled  | 
}  | 
}  | 
}  | 
extension MapViewController: UITableViewDataSource { | 
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | 
return items.count  | 
}  | 
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | 
let cell = tableView.dequeueReusableCell(withIdentifier: MapViewController.tableViewCellIdentifier, for: indexPath)  | 
let item = items[indexPath.row]  | 
cell.textLabel?.text = item.title  | 
return cell  | 
}  | 
}  | 
extension MapViewController: UITableViewDelegate { | 
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { | 
// Update the currently selected item.  | 
let item = items[indexPath.row]  | 
selectedItem = item  | 
// Hide the table view.  | 
animateTableView(hidden: true)  | 
}  | 
    func tableView(_ tableView: UITableView, didUpdateFocusIn context: UITableViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { | 
        if let indexPath = context.nextFocusedIndexPath { | 
let item = items[indexPath.row]  | 
highlightedItem = item  | 
}  | 
        else { | 
highlightedItem = nil  | 
}  | 
}  | 
}  | 
extension MapViewController: MKMapViewDelegate { | 
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { | 
        guard let searchResultMapAnnotation = annotation as? SearchResultMapAnnotation else { return nil } | 
let annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "pin")  | 
annotationView.canShowCallout = true  | 
        if searchResultMapAnnotation.item == highlightedItem { | 
annotationView.pinTintColor = MKPinAnnotationView.purplePinColor()  | 
}  | 
        else { | 
annotationView.pinTintColor = MKPinAnnotationView.redPinColor()  | 
}  | 
return annotationView  | 
}  | 
    func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { | 
        guard let annotation = view.annotation as? SearchResultMapAnnotation, tableViewHidden else { return } | 
// Update the currently selected item.  | 
selectedItem = annotation.item  | 
// Update the table view selection.  | 
        if let row = items.index(of: annotation.item) { | 
let indexPath = IndexPath(row: row, section: 0)  | 
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .middle)  | 
}  | 
}  | 
}  | 
extension MapViewController: UIGestureRecognizerDelegate { | 
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { | 
/*  | 
Only allow the select button recognizer to begin if the selected  | 
annotation's is also selected.  | 
*/  | 
        guard let annotation = mapView.selectedAnnotations.first as? SearchResultMapAnnotation, let annotationView = mapView.view(for: annotation), gestureRecognizer == selectGestureRecognizer else { | 
return true  | 
}  | 
return annotationView.isSelected  | 
}  | 
}  | 
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-10-04