问题描述
我目前正在使用 Swift 和 Firebase 构建 Uber 克隆,跟随 Stephan Dowless 的 course on Udemy 以了解有关 MapKit 的更多信息,到目前为止进展顺利,但我正在努力向地图,显示从用户当前位置到注释的路线(通过点击 tableView 中显示的搜索结果之一添加到 mapView)。
我在 SO 上寻找了其他类似的问题,但没有找到任何可以回答我的问题的问题。我还尝试克隆其他使用此功能的项目(the completed Uber clone from the Udemy course、this Ray Wenderlich tutorial 和 this article on polylines using SwiftUI)来检查是否是我的代码有问题,但它们都存在相同的问题,即注释出现在屏幕上,但根本不出现叠加层。
在我的应用中,点击 this screen 中 tableView 上的“Starbucks”会产生 this screen(显示星巴克和用户当前位置的注释,但没有叠加)。
同样,运行前面提到的 SwiftUI MapKit tutorial app from Medium 会显示 this(两个注释但没有叠加)。
这让我相信这是我这边出了问题。我也试过在我的手机 (iPhone 7) 上运行这些应用,但遇到了同样的问题。
以下是相关的代码行:
属性声明,包括 route 和 mapView
quick brown fox
mapView 委托设置为 self(确认用户登录时调用的函数)
// MARK:- Properties
private let mapView = MKMapView()
private var searchResults = [MKPlacemark]()
private var route: MKRoute?
MapView 函数
private func configureMapView() {
view.addSubview(mapView)
mapView.frame = view.frame
mapView.delegate = self
mapView.showsUserLocation = true
mapView.userTrackingMode = .follow
}
TableView didSelectRowAt 方法
// MARK:- MapView Functions
extension HomeViewController: MKMapViewDelegate {
private func generatePolyline(toDestination destination: MKMapItem) {
let request = MKDirections.Request()
request.source = MKMapItem.forCurrentLocation()
request.destination = destination
request.transportType = .automobile
let directionRequest = MKDirections(request: request)
directionRequest.calculate { (response,error) in
guard let response = response else { return }
self.route = response.routes[0]
guard let polyline = self.route?.polyline else { return }
self.mapView.addOverlay(polyline)
}
}
private func searchBy(naturalLanguageQuery: String,completion: @escaping([MKPlacemark]) -> Void) {
var results = [MKPlacemark]()
let request = MKLocalSearch.Request()
request.region = mapView.region
request.naturalLanguageQuery = naturalLanguageQuery
let search = MKLocalSearch(request: request)
search.start { (response,error) in
guard let response = response else { return }
response.mapItems.forEach { (item) in
results.append(item.placemark)
}
completion(results)
}
}
// Change driver annotation appearance to Uber arrow
public func mapView(_ mapView: MKMapView,viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if let annotation = annotation as? DriverAnnotation {
let view = MKAnnotationView(annotation: annotation,reuseIdentifier: DriverAnnotation.identifier)
view.image = #imageLiteral(resourceName: "chevron-sign-to-right")
return view
}
return nil
}
func mapView(_ mapView: MKMapView,rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let route = self.route {
let polyline = route.polyline
let lineRenderer = MKPolylineRenderer(overlay: polyline)
lineRenderer.strokeColor = .mainBlueTint
lineRenderer.lineWidth = 4
return lineRenderer
}
return MKOverlayRenderer()
}
}
最后,这是整个视图控制器,以防万一我错过了上面的任何内容。如果回答问题需要更多信息,请告诉我,我会提供。
整个视图控制器
func tableView(_ tableView: UITableView,didSelectRowAt indexPath: IndexPath) {
let placemark = self.searchResults[indexPath.row]
configureActionButtonState(config: .dismissActionView)
let destination = MKMapItem(placemark: placemark)
self.generatePolyline(toDestination: destination)
self.dismissInputView { _ in
let annotation = MKPointAnnotation()
annotation.coordinate = placemark.coordinate
self.mapView.addAnnotation(annotation)
self.mapView.selectAnnotation(annotation,animated: true)
}
}
这也是调试窗口中的输出,不确定它是否相关:
import UIKit
import Firebase
import MapKit
private enum ActionButtonConfiguration {
case showMenu
case dismissActionView
init() {
self = .showMenu
}
}
class HomeViewController: UIViewController {
// MARK:- Properties
private let mapView = MKMapView()
private let locationManager = LocationHandler.shared.locationManager
private let inputActivationView = LocationInputActivationView()
private let locationInputView = LocationInputView()
private var searchResults = [MKPlacemark]()
private var actionButtonConfig = ActionButtonConfiguration()
private var route: MKRoute?
private let tableView = UITableView()
private var user: User? {
didSet {
locationInputView.user = user
}
}
private let actionButton: UIButton = {
let button = UIButton()
button.setImage(#imageLiteral(resourceName: "baseline_menu_black_36dp").withRenderingMode(.alwaysOriginal),for: .normal)
button.addTarget(self,action: #selector(didTapActionButton),for: .touchUpInside)
return button
}()
// MARK:- Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
checkIfUserLoggedIn()
locationManagerDidChangeAuthorization(locationManager!)
}
// MARK:- Selectors
@objc private func didTapActionButton() {
switch actionButtonConfig {
case .showMenu:
print("Show menu")
case .dismissActionView:
removeAnnotationsAndOverlays()
UIView.animate(withDuration: 0.3) {
self.configureActionButtonState(config: .showMenu)
self.inputActivationView.alpha = 1
}
}
}
// MARK:- API
private func fetchUserData() {
guard let currentUserId = Auth.auth().currentUser?.uid else { return }
Service.shared.fetchUserData(uid: currentUserId) { user in
self.user = user
}
}
// N.B. Service.shared.fetchDrivers automatically gets called every time the location of the driver changes since it is observing the database via geofire (see definition of this within Service.swift)
private func fetchDrivers() {
guard let location = locationManager?.location else { return }
Service.shared.fetchDrivers(location: location) { (driver) in
guard let coordinate = driver.location?.coordinate else { return }
let annotation = DriverAnnotation(uid: driver.uid,coordinate: coordinate)
var driverIsVisible: Bool {
return self.mapView.annotations.contains { annotation -> Bool in
guard let driverAnnotation = annotation as? DriverAnnotation else { return false }
if driverAnnotation.uid == driver.uid {
// Driver is already visible - update driver location whenever this function is called
driverAnnotation.updateAnnotationPosition(withCoordinate: coordinate)
return true
}
// Driver is not visible
return false
}
}
// If driver is not visible then add to map
if !driverIsVisible {
self.mapView.addAnnotation(annotation)
}
}
}
private func checkIfUserLoggedIn() {
if Auth.auth().currentUser == nil {
// User is not logged in
print("DEBUG: User is not logged in")
DispatchQueue.main.async {
let nav = UINavigationController(rootViewController: LoginViewController())
nav.isModalInPresentation = true
nav.modalPresentationStyle = .fullScreen
self.present(nav,animated: true,completion: nil)
}
} else {
// User is logged in
configure()
}
}
private func logOut() {
do {
try Auth.auth().signOut()
DispatchQueue.main.async {
let nav = UINavigationController(rootViewController: LoginViewController())
nav.isModalInPresentation = true
nav.modalPresentationStyle = .fullScreen
self.present(nav,completion: nil)
}
} catch {
print("DEBUG: Error signing user out: \(error)")
}
}
// MARK:- Public Helper Functions
public func configure() {
configureUI()
fetchUserData()
fetchDrivers()
}
public func configureUI() {
configureMapView()
configureActionButton()
configureInputActivationView()
configureTableView()
}
// MARK:- Private Helper Functions
private func configureActionButton() {
view.addSubview(actionButton)
actionButton.anchor(top: view.safeAreaLayoutGuide.topAnchor,left: view.safeAreaLayoutGuide.leftAnchor,paddingTop: 16,paddingLeft: 16,width: 30,height: 30)
}
private func configureActionButtonState(config: ActionButtonConfiguration) {
switch config {
case .showMenu:
self.actionButton.setImage(#imageLiteral(resourceName: "baseline_menu_black_36dp").withRenderingMode(.alwaysOriginal),for: .normal)
self.actionButtonConfig = .showMenu
case .dismissActionView:
self.actionButton.setImage(#imageLiteral(resourceName: "baseline_arrow_back_black_36dp-1").withRenderingMode(.alwaysOriginal),for: .normal)
self.actionButtonConfig = .dismissActionView
}
}
private func configureMapView() {
view.addSubview(mapView)
mapView.frame = view.frame
mapView.delegate = self
mapView.showsUserLocation = true
mapView.userTrackingMode = .follow
}
private func configureInputActivationView() {
inputActivationView.delegate = self
view.addSubview(inputActivationView)
inputActivationView.centerX(inView: view)
inputActivationView.anchor(top: actionButton.bottomAnchor,right: view.safeAreaLayoutGuide.rightAnchor,paddingTop: 18,paddingLeft: 20,paddingRight: 20,height: 40)
// Animate inputActivationView (fade in)
inputActivationView.alpha = 0
UIView.animate(withDuration: 2) {
self.inputActivationView.alpha = 1
}
}
private func configureLocationInputView() {
locationInputView.delegate = self
view.addSubview(locationInputView)
locationInputView.anchor(top: view.topAnchor,left: view.leftAnchor,right: view.rightAnchor,height: 200)
locationInputView.alpha = 0
UIView.animate(withDuration: 0.5) {
self.locationInputView.alpha = 1
} completion: { _ in
print("DEBUG: Present table view")
UIView.animate(withDuration: 0.3) {
self.tableView.frame.origin.y = self.locationInputView.frame.height
}
}
}
private func configureTableView() {
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
tableView.register(LocationTableViewCell.self,forCellReuseIdentifier: LocationTableViewCell.identifier)
tableView.rowHeight = 60
tableView.tableFooterView = UIView()
let height = view.frame.height - locationInputView.frame.height
tableView.frame = CGRect(x: 0,y: view.frame.height,width: view.frame.width,height: height)
}
private func dismissInputView(completion: ((Bool) -> Void)? = nil) {
UIView.animate(withDuration: 0.3,animations: {
self.locationInputView.alpha = 0
self.tableView.frame.origin.y = self.view.frame.height
self.locationInputView.removeFromSuperview()
},completion: completion)
}
private func removeAnnotationsAndOverlays() {
mapView.annotations.forEach { annotation in
if let anno = annotation as? MKPointAnnotation {
mapView.removeAnnotation(anno)
}
}
if mapView.overlays.count > 0 {
mapView.removeOverlay(mapView.overlays[0])
}
}
}
// MARK:- MapView Functions
extension HomeViewController: MKMapViewDelegate {
private func generatePolyline(toDestination destination: MKMapItem) {
let request = MKDirections.Request()
request.source = MKMapItem.forCurrentLocation()
request.destination = destination
request.transportType = .automobile
let directionRequest = MKDirections(request: request)
directionRequest.calculate { (response,rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let route = self.route {
let polyline = route.polyline
let lineRenderer = MKPolylineRenderer(overlay: polyline)
lineRenderer.strokeColor = .mainBlueTint
lineRenderer.lineWidth = 3
return lineRenderer
}
return MKOverlayRenderer()
}
}
// MARK:- Location Manager Services
extension HomeViewController {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .notDetermined:
print("DEBUG: Not determined")
locationManager?.requestWhenInUseAuthorization()
case .restricted:
break
case .denied:
break
case .authorizedAlways:
print("DEBUG: Auth always")
locationManager?.startUpdatingLocation()
locationManager?.desiredAccuracy = kCLLocationAccuracyBest
case .authorizedWhenInUse:
print("DEBUG: Auth when in use")
locationManager?.requestAlwaysAuthorization()
@unknown default:
break
}
}
}
// MARK:- Input Activation View Delegate Methods
extension HomeViewController: LocationInputActivationViewDelegate {
func presentLocationInputView() {
configureLocationInputView()
self.inputActivationView.alpha = 0
}
}
// MARK:- Input View Delegate Methods
extension HomeViewController: LocationInputViewDelegate {
func executeSearch(query: String) {
searchBy(naturalLanguageQuery: query) { (results) in
print("DEBUG: Placemarks are \(results)")
self.searchResults = results
self.tableView.reloadData()
}
}
func dismissLocationInputView() {
dismissInputView()
UIView.animate(withDuration: 0.5) {
self.inputActivationView.alpha = 1
}
}
}
// MARK:- TableView Delegate and Datasource Methods
extension HomeViewController: UITableViewDelegate,UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
2
}
func tableView(_ tableView: UITableView,titleForHeaderInSection section: Int) -> String? {
return "test"
}
func tableView(_ tableView: UITableView,numberOfRowsInSection section: Int) -> Int {
return section == 0 ? 2 : searchResults.count
}
func tableView(_ tableView: UITableView,cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: LocationTableViewCell.identifier,for: indexPath) as! LocationTableViewCell
if indexPath.section == 1 {
cell.placemark = searchResults[indexPath.row]
}
return cell
}
func tableView(_ tableView: UITableView,animated: true)
}
}
}
提前致谢!
解决方法
我已经创建了一个简化版本 - 使用硬编码的目的地,但按照您的方式创建路线,这非常有效。
private func generatePolyline(toDestination destination: MKMapItem) {
let request = MKDirections.Request()
request.source = MKMapItem.forCurrentLocation()
request.destination = destination
request.transportType = .automobile
let directionRequest = MKDirections(request: request)
directionRequest.calculate { (response,error) in
guard let response = response else { return }
self.route = response.routes[0]
guard let polyline = self.route?.polyline else { return }
self.mapView.addOverlay(polyline)
print("update map?")
}
}
我在这里添加的唯一内容是最后的调试打印语句 - 总是很高兴知道某些事情应该发生,但知道它确实发生了总是很高兴!
我从一个简单的测试按钮调用了这个函数
@IBAction func cmdButton(_ sender: Any) {
// for testing - Edinburgh Castle
generatePolyline(toDestination: MKMapItem(placemark: MKPlacemark(coordinate: CLLocationCoordinate2D(latitude: 55.9483,longitude: -3.1981),addressDictionary: nil)))
}
渲染器非常简单,包括上面的测试打印
func mapView(_ mapView: MKMapView,rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let pr = MKPolylineRenderer(overlay: overlay)
pr.strokeColor = UIColor.red.withAlphaComponent(0.5)
pr.lineWidth = 7
print("rendererFor overlay")
return pr
}
如果您看到调试打印注释,您应该没问题。让我知道你的进展如何...
,深入挖掘后,原因是我在柬埔寨金边。 Apple Maps 无法在柬埔寨提供方向。如果我想从一个点到另一个点画一条线,我猜我将不得不求助于谷歌地图。