版本记录
版本号 | 时间 |
---|---|
V1.0 | 2020.06.20 星期六 |
前言
MapKit框架直接从您的应用界面显示地图或卫星图像,调出兴趣点,并确定地图坐标的地标信息。接下来几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. MapKit框架详细解析(一) —— 基本概览(一)
2. MapKit框架详细解析(二) —— 基本使用简单示例(一)
3. MapKit框架详细解析(三) —— 基本使用简单示例(二)
4. MapKit框架详细解析(四) —— 一个叠加视图相关的简单示例(一)
5. MapKit框架详细解析(五) —— 一个叠加视图相关的简单示例(二)
6. MapKit框架详细解析(六) —— 添加自定义图块(一)
7. MapKit框架详细解析(七) —— 添加自定义图块(二)
8. MapKit框架详细解析(八) —— 添加自定义图块(三)
9. MapKit框架详细解析(九) —— 地图特定区域放大和创建自定义地图annotations(一)
10. MapKit框架详细解析(十) —— 地图特定区域放大和创建自定义地图annotations(二)
11. MapKit框架详细解析(十一) —— 自定义MapKit Tiles(一)
12. MapKit框架详细解析(十二) —— 自定义MapKit Tiles(二)
13. MapKit框架详细解析(十三) —— MapKit Overlay Views(一)
14. MapKit框架详细解析(十四) —— MapKit Overlay Views(二)
15. MapKit框架详细解析(十五) —— 基于MapKit和Core Location的Routing(一)
源码
1. Swift
首先看下工程组织结构
下面就是源码了
1. AppDelegate.swift
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow()
window?.rootViewController = RouteSelectionViewController()
window?.overrideUserInterfaceStyle = .light
window?.makeKeyAndVisible()
// Override point for customization after application launch.
return true
}
}
2. CLPlacemark+Additions.swift
import CoreLocation
extension CLPlacemark {
var abbreviation: String {
if let name = self.name {
return name
}
if let interestingPlace = areasOfInterest?.first {
return interestingPlace
}
return [subThoroughfare, thoroughfare].compactMap { $0 }.joined(separator: " ")
}
}
3. TimeInterval+Additions.swift
import Foundation
extension TimeInterval {
var formatted: String {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.allowedUnits = [.hour, .minute]
return formatter.string(from: self) ?? ""
}
}
4. UIColor+Additions.swift
import UIKit
extension UIColor {
static var border: UIColor {
// swiftlint:disable:next force_unwrapping
return UIColor(named: "ui-border")!
}
static var primary: UIColor {
// swiftlint:disable:next force_unwrapping
return UIColor(named: "rw-green")!
}
}
5. UIButton+Additions.swift
import UIKit
extension UIButton {
func stylize() {
setTitleColor(.white, for: .normal)
setBackgroundImage(.buttonBackground, for: .normal)
titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
}
}
6. UIImage+Additions.swift
import UIKit
extension UIImage {
static var buttonBackground: UIImage {
let imageSideLength: CGFloat = 8
let halfSideLength = imageSideLength / 2
let imageFrame = CGRect(
x: 0,
y: 0,
width: imageSideLength,
height: imageSideLength
)
let image = UIGraphicsImageRenderer(size: imageFrame.size).image { ctx in
ctx.cgContext.addPath(
UIBezierPath(
roundedRect: imageFrame,
cornerRadius: halfSideLength
).cgPath
)
ctx.cgContext.setFillColor(UIColor.primary.cgColor)
ctx.cgContext.fillPath()
}
return image.resizableImage(
withCapInsets: UIEdgeInsets(
top: halfSideLength,
left: halfSideLength,
bottom: halfSideLength,
right: halfSideLength
)
)
}
}
7. UIView+Additions.swift
import UIKit
extension UIView {
func addBorder() {
layer.borderWidth = 1
layer.cornerRadius = 3
layer.borderColor = UIColor.border.cgColor
}
}
8. UITextField+Additions.swift
import UIKit
extension UITextField {
var contents: String? {
guard
let text = text?.trimmingCharacters(in: .whitespaces),
!text.isEmpty
else {
return nil
}
return text
}
}
9. RouteBuilder.swift
import MapKit
enum RouteBuilder {
enum Segment {
case text(String)
case location(CLLocation)
}
enum RouteError: Error {
case invalidSegment(String)
}
typealias PlaceCompletionBlock = (MKPlacemark?) -> Void
typealias RouteCompletionBlock = (Result) -> Void
private static let routeQueue = DispatchQueue(label: "com.raywenderlich.RWRouter.route-builder")
static func buildRoute(origin: Segment, stops: [Segment], within region: MKCoordinateRegion?, completion: @escaping RouteCompletionBlock) {
routeQueue.async {
let group = DispatchGroup()
var originItem: MKMapItem?
group.enter()
requestPlace(for: origin, within: region) { place in
if let requestedPlace = place {
originItem = MKMapItem(placemark: requestedPlace)
}
group.leave()
}
var stopItems = [MKMapItem](repeating: .init(), count: stops.count)
for (index, stop) in stops.enumerated() {
group.enter()
requestPlace(for: stop, within: region) { place in
if let requestedPlace = place {
stopItems[index] = MKMapItem(placemark: requestedPlace)
}
group.leave()
}
}
group.notify(queue: .main) {
if let originMapItem = originItem, !stopItems.isEmpty {
let route = Route(origin: originMapItem, stops: stopItems)
completion(.success(route))
} else {
let reason = originItem == nil ? "the origin address" : "one or more of the stops"
completion(.failure(.invalidSegment(reason)))
}
}
}
}
private static func requestPlace(for segment: Segment, within region: MKCoordinateRegion?, completion: @escaping PlaceCompletionBlock) {
if case .text(let value) = segment, let nearbyRegion = region {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = value
request.region = nearbyRegion
MKLocalSearch(request: request).start { response, _ in
let place: MKPlacemark?
if let firstItem = response?.mapItems.first {
place = firstItem.placemark
} else {
place = nil
}
completion(place)
}
} else {
CLGeocoder().geocodeSegment(segment) { places, _ in
let place: MKPlacemark?
if let firstPlace = places?.first {
place = MKPlacemark(placemark: firstPlace)
} else {
place = nil
}
completion(place)
}
}
}
}
private extension CLGeocoder {
func geocodeSegment(_ segment: RouteBuilder.Segment, completionHandler: @escaping CLGeocodeCompletionHandler) {
switch segment {
case .text(let value):
geocodeAddressString(value, completionHandler: completionHandler)
case .location(let value):
reverseGeocodeLocation(value, completionHandler: completionHandler)
}
}
}
10. Route.swift
import MapKit
struct Route {
let origin: MKMapItem
let stops: [MKMapItem]
var annotations: [MKAnnotation] {
var annotations: [MKAnnotation] = []
annotations.append(
RouteAnnotation(item: origin)
)
annotations.append(contentsOf: stops.map { stop in
return RouteAnnotation(item: stop)
})
return annotations
}
var label: String {
if let name = stops.first?.name, stops.count == 1 {
return "Directions to \(name)"
} else {
let stopNames = stops.compactMap { stop in
return stop.name
}
let namesString = stopNames.joined(separator: " and ")
return "Directions between \(namesString)"
}
}
}
11. RouteAnnotation.swift
import MapKit
class RouteAnnotation: NSObject {
private let item: MKMapItem
init(item: MKMapItem) {
self.item = item
super.init()
}
}
// MARK: - MKAnnotation
extension RouteAnnotation: MKAnnotation {
var coordinate: CLLocationCoordinate2D {
return item.placemark.coordinate
}
var title: String? {
return item.name
}
}
12. DirectionsViewController.swift
import UIKit
import MapKit
class DirectionsViewController: UIViewController {
@IBOutlet private var mapView: MKMapView!
@IBOutlet private var headerLabel: UILabel!
@IBOutlet private var tableView: UITableView!
@IBOutlet private var informationLabel: UILabel!
@IBOutlet private var activityIndicatorView: UIActivityIndicatorView!
private let cellIdentifier = "DirectionsCell"
private let distanceFormatter = MKDistanceFormatter()
private let route: Route
private var mapRoutes: [MKRoute] = []
private var totalTravelTime: TimeInterval = 0
private var totalDistance: CLLocationDistance = 0
private var groupedRoutes: [(startItem: MKMapItem, endItem: MKMapItem)] = []
init(route: Route) {
self.route = route
super.init(nibName: String(describing: DirectionsViewController.self), bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
groupAndRequestDirections()
headerLabel.text = route.label
tableView.dataSource = self
mapView.delegate = self
mapView.showAnnotations(route.annotations, animated: false)
}
// MARK: - Helpers
private func groupAndRequestDirections() {
guard let firstStop = route.stops.first else {
return
}
groupedRoutes.append((route.origin, firstStop))
if route.stops.count == 2 {
let secondStop = route.stops[1]
groupedRoutes.append((firstStop, secondStop))
groupedRoutes.append((secondStop, route.origin))
}
fetchNextRoute()
}
private func fetchNextRoute() {
guard !groupedRoutes.isEmpty else {
activityIndicatorView.stopAnimating()
return
}
let nextGroup = groupedRoutes.removeFirst()
let request = MKDirections.Request()
request.source = nextGroup.startItem
request.destination = nextGroup.endItem
let directions = MKDirections(request: request)
directions.calculate { response, error in
guard let mapRoute = response?.routes.first else {
self.informationLabel.text = error?.localizedDescription
self.activityIndicatorView.stopAnimating()
return
}
self.updateView(with: mapRoute)
self.fetchNextRoute()
}
}
private func updateView(with mapRoute: MKRoute) {
let padding: CGFloat = 8
mapView.addOverlay(mapRoute.polyline)
mapView.setVisibleMapRect(
mapView.visibleMapRect.union(
mapRoute.polyline.boundingMapRect
),
edgePadding: UIEdgeInsets(
top: 0,
left: padding,
bottom: padding,
right: padding
),
animated: true
)
totalDistance += mapRoute.distance
totalTravelTime += mapRoute.expectedTravelTime
let informationComponents = [
totalTravelTime.formatted,
"• \(distanceFormatter.string(fromDistance: totalDistance))"
]
informationLabel.text = informationComponents.joined(separator: " ")
mapRoutes.append(mapRoute)
tableView.reloadData()
}
}
// MARK: - UITableViewDataSource
extension DirectionsViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return mapRoutes.isEmpty ? 0 : mapRoutes.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let route = mapRoutes[section]
return route.steps.count - 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = { () -> UITableViewCell in
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) else {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
cell.selectionStyle = .none
return cell
}
return cell
}()
let route = mapRoutes[indexPath.section]
let step = route.steps[indexPath.row + 1]
cell.textLabel?.text = "\(indexPath.row + 1): \(step.notice ?? step.instructions)"
cell.detailTextLabel?.text = distanceFormatter.string(
fromDistance: step.distance
)
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let route = mapRoutes[section]
return route.name
}
}
// MARK: - MKMapViewDelegate
extension DirectionsViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = .systemBlue
renderer.lineWidth = 3
return renderer
}
}
13. RouteSelectionViewController.swift
import UIKit
import MapKit
import CoreLocation
class RouteSelectionViewController: UIViewController {
@IBOutlet private var inputContainerView: UIView!
@IBOutlet private var originTextField: UITextField!
@IBOutlet private var stopTextField: UITextField!
@IBOutlet private var extraStopTextField: UITextField!
@IBOutlet private var calculateButton: UIButton!
@IBOutlet private var activityIndicatorView: UIActivityIndicatorView!
@IBOutlet private var keyboardAvoidingConstraint: NSLayoutConstraint!
@IBOutlet private var suggestionLabel: UILabel!
@IBOutlet private var suggestionContainerView: UIView!
@IBOutlet private var suggestionContainerTopConstraint: NSLayoutConstraint!
private var editingTextField: UITextField?
private var currentRegion: MKCoordinateRegion?
private var currentPlace: CLPlacemark?
private let locationManager = CLLocationManager()
private let completer = MKLocalSearchCompleter()
private let defaultAnimationDuration: TimeInterval = 0.25
override func viewDidLoad() {
super.viewDidLoad()
suggestionContainerView.addBorder()
inputContainerView.addBorder()
calculateButton.stylize()
completer.delegate = self
beginObserving()
configureGestures()
configureTextFields()
attemptLocationAccess()
hideSuggestionView(animated: false)
}
// MARK: - Helpers
private func configureGestures() {
view.addGestureRecognizer(
UITapGestureRecognizer(
target: self,
action: #selector(handleTap(_:))
)
)
suggestionContainerView.addGestureRecognizer(
UITapGestureRecognizer(
target: self,
action: #selector(suggestionTapped(_:))
)
)
}
private func configureTextFields() {
originTextField.delegate = self
stopTextField.delegate = self
extraStopTextField.delegate = self
originTextField.addTarget(
self,
action: #selector(textFieldDidChange(_:)),
for: .editingChanged
)
stopTextField.addTarget(
self,
action: #selector(textFieldDidChange(_:)),
for: .editingChanged
)
extraStopTextField.addTarget(
self,
action: #selector(textFieldDidChange(_:)),
for: .editingChanged
)
}
private func attemptLocationAccess() {
guard CLLocationManager.locationServicesEnabled() else {
return
}
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.delegate = self
if CLLocationManager.authorizationStatus() == .notDetermined {
locationManager.requestWhenInUseAuthorization()
} else {
locationManager.requestLocation()
}
}
private func hideSuggestionView(animated: Bool) {
suggestionContainerTopConstraint.constant = -1 * (suggestionContainerView.bounds.height + 1)
guard animated else {
view.layoutIfNeeded()
return
}
UIView.animate(withDuration: defaultAnimationDuration) {
self.view.layoutIfNeeded()
}
}
private func showSuggestion(_ suggestion: String) {
suggestionLabel.text = suggestion
suggestionContainerTopConstraint.constant = -4 // to hide the top corners
UIView.animate(withDuration: defaultAnimationDuration) {
self.view.layoutIfNeeded()
}
}
private func presentAlert(message: String) {
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
present(alertController, animated: true)
}
// MARK: - Actions
@objc private func textFieldDidChange(_ field: UITextField) {
if field == originTextField && currentPlace != nil {
currentPlace = nil
field.text = ""
}
guard let query = field.contents else {
hideSuggestionView(animated: true)
if completer.isSearching {
completer.cancel()
}
return
}
completer.queryFragment = query
}
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
let gestureView = gesture.view
let point = gesture.location(in: gestureView)
guard
let hitView = gestureView?.hitTest(point, with: nil),
hitView == gestureView
else {
return
}
view.endEditing(true)
}
@objc private func suggestionTapped(_ gesture: UITapGestureRecognizer) {
hideSuggestionView(animated: true)
editingTextField?.text = suggestionLabel.text
editingTextField = nil
}
@IBAction private func calculateButtonTapped() {
view.endEditing(true)
calculateButton.isEnabled = false
activityIndicatorView.startAnimating()
let segment: RouteBuilder.Segment?
if let currentLocation = currentPlace?.location {
segment = .location(currentLocation)
} else if let originValue = originTextField.contents {
segment = .text(originValue)
} else {
segment = nil
}
let stopSegments: [RouteBuilder.Segment] = [
stopTextField.contents,
extraStopTextField.contents
]
.compactMap { contents in
if let value = contents {
return .text(value)
} else {
return nil
}
}
guard
let originSegment = segment,
!stopSegments.isEmpty
else {
presentAlert(message: "Please select an origin and at least 1 stop.")
activityIndicatorView.stopAnimating()
calculateButton.isEnabled = true
return
}
RouteBuilder.buildRoute(
origin: originSegment,
stops: stopSegments,
within: currentRegion
) { result in
self.calculateButton.isEnabled = true
self.activityIndicatorView.stopAnimating()
switch result {
case .success(let route):
let viewController = DirectionsViewController(route: route)
self.present(viewController, animated: true)
case .failure(let error):
let errorMessage: String
switch error {
case .invalidSegment(let reason):
errorMessage = "There was an error with: \(reason)."
}
self.presentAlert(message: errorMessage)
}
}
}
// MARK: - Notifications
private func beginObserving() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleKeyboardFrameChange(_:)),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil
)
}
@objc private func handleKeyboardFrameChange(_ notification: Notification) {
guard let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
return
}
let viewHeight = view.bounds.height - view.safeAreaInsets.bottom
let visibleHeight = viewHeight - frame.origin.y
keyboardAvoidingConstraint.constant = visibleHeight + 32
UIView.animate(withDuration: defaultAnimationDuration) {
self.view.layoutIfNeeded()
}
}
}
// MARK: - UITextFieldDelegate
extension RouteSelectionViewController: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
hideSuggestionView(animated: true)
if completer.isSearching {
completer.cancel()
}
editingTextField = textField
}
}
// MARK: - CLLocationManagerDelegate
extension RouteSelectionViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
guard status == .authorizedWhenInUse else {
return
}
manager.requestLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let firstLocation = locations.first else {
return
}
let commonDelta: CLLocationDegrees = 25 / 111 // 1/111 = 1 latitude km
let span = MKCoordinateSpan(latitudeDelta: commonDelta, longitudeDelta: commonDelta)
let region = MKCoordinateRegion(center: firstLocation.coordinate, span: span)
currentRegion = region
completer.region = region
CLGeocoder().reverseGeocodeLocation(firstLocation) { places, _ in
guard let firstPlace = places?.first, self.originTextField.contents == nil else {
return
}
self.currentPlace = firstPlace
self.originTextField.text = firstPlace.abbreviation
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Error requesting location: \(error.localizedDescription)")
}
}
// MARK: - MKLocalSearchCompleterDelegate
extension RouteSelectionViewController: MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
guard let firstResult = completer.results.first else {
return
}
showSuggestion(firstResult.title)
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
print("Error suggesting a location: \(error.localizedDescription)")
}
}
后记
本篇主要讲述了基于
MapKit
和Core Location
的Routing
,感兴趣的给个赞或者关注~~~