SwiftUI集成MapKit逐向导航无地图显示问题求助
问题描述
我正尝试基于MapKit实现一款逐向导航应用,发现现有相关实现多基于Storyboard而非SwiftUI。参考了相关代码后,运行应用时无地图显示且无报错,怀疑问题与IBOutlet变量有关,希望找到在SwiftUI中复用该ViewController代码的方法,原代码如下:
import SwiftUI import MapboxMaps import MapboxDirections import MapboxCoreNavigation import MapboxNavigation import CoreLocation import MapKit import AVFoundation struct maps: View { var body: some View { MapViewWrapper() .padding(5.0) } } struct maps_Previews: PreviewProvider { static var previews: some View { maps() } } struct MapViewWrapper : UIViewControllerRepresentable { func makeUIViewController(context: Context) -> ViewController { return ViewController() } func updateUIViewController(_ uiViewController: ViewController, context: Context) { } } class ViewController: UIViewController { @IBOutlet weak var directionsLabel: UILabel! @IBOutlet weak var searchBar: UISearchBar! @IBOutlet weak var mapView: MKMapView! let locationManager = CLLocationManager() var currentCoordinates: CLLocationCoordinate2D! var steps = [MKRoute.Step]() let speechSynthesizer = AVSpeechSynthesizer() var stepCounter = 0 override func viewDidLoad() { super.viewDidLoad() setupLocationManager() } fileprivate func setupLocationManager() { locationManager.requestAlwaysAuthorization() locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation locationManager.startUpdatingLocation() } func getDirections(to destination: MKMapItem) { let sourcePlacemark = MKPlacemark(coordinate: currentCoordinates) let sourceMapItem = MKMapItem(placemark: sourcePlacemark) let directionRequest = MKDirections.Request() directionRequest.source = sourceMapItem directionRequest.destination = destination directionRequest.transportType = .automobile let directions = MKDirections(request: directionRequest) directions.calculate { [weak self] (response, error) in if let error = error { return } guard let response = response else { return } guard let mainRoute = response.routes.first else { return } self?.mapView.addOverlay(mainRoute.polyline) self?.steps = mainRoute.steps self?.locationManager.monitoredRegions.forEach({ self?.locationManager.stopMonitoring(for: $0) }) for i in 0..<mainRoute.steps.count { let step = mainRoute.steps[i] print(step.instructions) print(step.distance) let region = CLCircularRegion(center: step.polyline.coordinate, radius: 20, identifier: "\(i)") self?.locationManager.startMonitoring(for: region) let circle = MKCircle(center: region.center, radius: region.radius) self?.mapView.addOverlay(circle) } let initialMessage = "In \(self?.steps[0].distance ?? 0) metres, \(self?.steps[0].instructions ?? "") then in \(self?.steps[1].distance ?? 0) metres, \(self?.steps[1].instructions ?? "")" self?.directionsLabel.text = initialMessage let speechUtterance = AVSpeechUtterance(string: initialMessage) self?.speechSynthesizer.speak(speechUtterance) self?.stepCounter += 1 } } } extension ViewController: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { manager.startUpdatingLocation() guard let currentLocation = locations.first else { return } currentCoordinates = currentLocation.coordinate } func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { print("Entered region!") stepCounter += 1 if stepCounter < steps.count { let currentStep = steps[stepCounter] let message = "In \(currentStep.distance) metres, \(currentStep.instructions)" self.directionsLabel.text = message let speechUtterance = AVSpeechUtterance(string: message) speechSynthesizer.speak(speechUtterance) } else { let message = "Arrived at destination" directionsLabel.text = message let speechUtterance = AVSpeechUtterance(string: message) speechSynthesizer.speak(speechUtterance) stepCounter = 0 self.locationManager.monitoredRegions.forEach { self.locationManager.stopMonitoring(for: $0) } } } } extension ViewController: UISearchBarDelegate { func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { let localSearchRequest = MKLocalSearch.Request() localSearchRequest.naturalLanguageQuery = searchBar.text let region = MKCoordinateRegion(center: currentCoordinates, span: MKCoordinateSpan.init(latitudeDelta: 0.1, longitudeDelta: 0.1)) localSearchRequest.region = region let localSearch = MKLocalSearch(request: localSearchRequest) localSearch.start { [weak self] (response, error) in if let error = error { return } guard let response = response else { return } print(response.mapItems) guard let firstMapItem = response.mapItems.first else { return } self?.getDirections(to: firstMapItem) } } } extension ViewController: MKMapViewDelegate { func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { if overlay is MKPolyline { let renderer = MKPolylineRenderer(overlay: overlay) renderer.strokeColor = .blue renderer.lineWidth = 10 return renderer } if overlay is MKCircle { let renderer = MKCircleRenderer(overlay: overlay) renderer.strokeColor = .orange renderer.fillColor = .red renderer.alpha = 0.5 return renderer } return MKOverlayRenderer() } }
解决方案
原代码的核心问题是@IBOutlet变量仅适用于Storyboard界面关联,纯代码创建的ViewController中这些变量未被实例化,导致界面无元素显示。以下是修改后的完整代码,通过纯代码创建并布局UI元素,适配SwiftUI嵌入场景:
import SwiftUI import CoreLocation import MapKit import AVFoundation struct MapsView: View { var body: some View { MapViewWrapper() .ignoresSafeArea() } } struct MapsView_Previews: PreviewProvider { static var previews: some View { MapsView() } } struct MapViewWrapper : UIViewControllerRepresentable { func makeUIViewController(context: Context) -> NavigationViewController { return NavigationViewController() } func updateUIViewController(_ uiViewController: NavigationViewController, context: Context) {} } class NavigationViewController: UIViewController { // 移除IBOutlet,手动创建UI组件 private let mapView = MKMapView() private let searchBar = UISearchBar() private let directionsLabel = UILabel() let locationManager = CLLocationManager() var currentCoordinates: CLLocationCoordinate2D! var steps = [MKRoute.Step]() let speechSynthesizer = AVSpeechSynthesizer() var stepCounter = 0 override func viewDidLoad() { super.viewDidLoad() setupUI() setupLocationManager() } // 初始化并布局UI元素 private func setupUI() { // 配置mapView mapView.delegate = self mapView.userTrackingMode = .followWithHeading mapView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(mapView) // 配置searchBar searchBar.delegate = self searchBar.placeholder = "输入目的地" searchBar.translatesAutoresizingMaskIntoConstraints = false view.addSubview(searchBar) // 配置directionsLabel directionsLabel.backgroundColor = .white.withAlphaComponent(0.8) directionsLabel.numberOfLines = 0 directionsLabel.padding = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12) directionsLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(directionsLabel) // 设置AutoLayout约束 NSLayoutConstraint.activate([ searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), mapView.topAnchor.constraint(equalTo: searchBar.bottomAnchor), mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor), mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor), mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor), directionsLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), directionsLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), directionsLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16) ]) } fileprivate func setupLocationManager() { locationManager.requestAlwaysAuthorization() locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation locationManager.startUpdatingLocation() } func getDirections(to destination: MKMapItem) { guard let currentCoords = currentCoordinates else { return } let sourcePlacemark = MKPlacemark(coordinate: currentCoords) let sourceMapItem = MKMapItem(placemark: sourcePlacemark) let directionRequest = MKDirections.Request() directionRequest.source = sourceMapItem directionRequest.destination = destination directionRequest.transportType = .automobile let directions = MKDirections(request: directionRequest) directions.calculate { [weak self] (response, error) in if let error = error { print("路线计算错误: \(error.localizedDescription)") return } guard let response = response, let mainRoute = response.routes.first else { print("未获取到路线") return } // 清除旧的覆盖物 self?.mapView.removeOverlays(self?.mapView.overlays ?? []) self?.mapView.addOverlay(mainRoute.polyline) self?.steps = mainRoute.steps // 停止旧的区域监控 self?.locationManager.monitoredRegions.forEach { self?.locationManager.stopMonitoring(for: $0) } // 为每个路线步骤创建地理围栏 for i in 0..<mainRoute.steps.count { let step = mainRoute.steps[i] guard let stepCoord = step.polyline.coordinate as? CLLocationCoordinate2D else { continue } let region = CLCircularRegion(center: stepCoord, radius: 20, identifier: "\(i)") self?.locationManager.startMonitoring(for: region) let circle = MKCircle(center: region.center, radius: region.radius) self?.mapView.addOverlay(circle) } // 更新导航提示 let initialMessage: String if self?.steps.count ?? 0 >= 2 { initialMessage = "距离\(Int(self?.steps[0].distance ?? 0))米时,\(self?.steps[0].instructions ?? "");之后距离\(Int(self?.steps[1].distance ?? 0))米时,\(self?.steps[1].instructions ?? "")" } else if self?.steps.count ?? 0 == 1 { initialMessage = "距离\(Int(self?.steps[0].distance ?? 0))米时,\(self?.steps[0].instructions ?? "")" } else { initialMessage = "已到达目的地" } self?.directionsLabel.text = initialMessage let speechUtterance = AVSpeechUtterance(string: initialMessage) self?.speechSynthesizer.speak(speechUtterance) self?.stepCounter = 1 } } } extension NavigationViewController: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let currentLocation = locations.first else { return } currentCoordinates = currentLocation.coordinate // 仅首次定位时设置地图区域 if mapView.region.span.latitudeDelta == 0 { let region = MKCoordinateRegion(center: currentCoordinates, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)) mapView.setRegion(region, animated: true) } } func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { guard let regionID = Int(region.identifier), regionID < steps.count else { return } stepCounter = regionID + 1 let currentStep = steps[regionID] let message = "距离\(Int(currentStep.distance))米时,\(currentStep.instructions)" directionsLabel.text = message let speechUtterance = AVSpeechUtterance(string: message) speechSynthesizer.speak(speechUtterance) // 到达最后一个区域时提示终点 if stepCounter >= steps.count { let finishMessage = "已到达目的地" directionsLabel.text = finishMessage speechSynthesizer.speak(AVSpeechUtterance(string: finishMessage)) locationManager.monitoredRegions.forEach { locationManager.stopMonitoring(for: $0) } stepCounter = 0 } } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print("定位错误: \(error.localizedDescription)") } } extension NavigationViewController: UISearchBarDelegate { func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { searchBar.resignFirstResponder() guard let query = searchBar.text, !query.isEmpty, let currentCoords = currentCoordinates else { return } let localSearchRequest = MKLocalSearch.Request() localSearchRequest.naturalLanguageQuery = query localSearchRequest.region = MKCoordinateRegion(center: currentCoords, span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)) let localSearch = MKLocalSearch(request: localSearchRequest) localSearch.start { [weak self] (response, error) in if let error = error { print("搜索错误: \(error.localizedDescription)") return } guard let response = response, let firstMapItem = response.mapItems.first else { print("未找到匹配地点") return } self?.getDirections(to: firstMapItem) } } } extension NavigationViewController: MKMapViewDelegate { func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { if overlay is MKPolyline { let renderer = MKPolylineRenderer(overlay: overlay) renderer.strokeColor = .systemBlue renderer.lineWidth = 5 return renderer } if overlay is MKCircle { let renderer = MKCircleRenderer(overlay: overlay) renderer.strokeColor = .systemOrange renderer.fillColor = .systemRed.withAlphaComponent(0.3) renderer.lineWidth = 2 return renderer } return MKOverlayRenderer() } }
关键修改说明
- 替换IBOutlet为纯代码UI组件:移除原代码中的
@IBOutlet声明,手动创建MKMapView、UISearchBar和UILabel,并通过AutoLayout完成布局 - 完善地图配置:设置
mapView.delegate = self确保覆盖物渲染逻辑生效,添加userTrackingMode跟随用户位置 - 修复定位逻辑:移除原代码中
didUpdateLocations里重复调用的startUpdatingLocation,仅首次定位时初始化地图区域 - 增强错误处理:添加路线计算、搜索、定位的错误打印,方便调试
- 优化导航提示:根据步骤数量调整提示文本,避免数组越界
- 调整命名规范:将原
maps视图重命名为MapsView,ViewController重命名为NavigationViewController,符合Swift命名规范
内容的提问来源于stack exchange,提问作者Allyson Elwart
相关产品推荐
相关产品推荐

