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