You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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()
    }
}

关键修改说明

  1. 替换IBOutlet为纯代码UI组件:移除原代码中的@IBOutlet声明,手动创建MKMapView、UISearchBar和UILabel,并通过AutoLayout完成布局
  2. 完善地图配置:设置mapView.delegate = self确保覆盖物渲染逻辑生效,添加userTrackingMode跟随用户位置
  3. 修复定位逻辑:移除原代码中didUpdateLocations里重复调用的startUpdatingLocation,仅首次定位时初始化地图区域
  4. 增强错误处理:添加路线计算、搜索、定位的错误打印,方便调试
  5. 优化导航提示:根据步骤数量调整提示文本,避免数组越界
  6. 调整命名规范:将原maps视图重命名为MapsView,ViewController重命名为NavigationViewController,符合Swift命名规范

内容的提问来源于stack exchange,提问作者Allyson Elwart

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 20:40:26