版本记录
版本号 | 时间 |
---|---|
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
首先看下工程组织结构
![](https://img.haomeiwen.com/i3691932/25682cfb035d9742.png)
下面就是源码了
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<Route, RouteError>) -> 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
,感兴趣的给个赞或者关注~~~
![](https://img.haomeiwen.com/i3691932/3897e72d8e3a6474.png)
网友评论