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

SwiftUI Map标注箭头随用户旋转地图保持道路朝向的实现方法

问题描述

现有代码里,标注箭头已经能和朝向340度的道路对齐,设备自身旋转时箭头也会同步旋转保持对齐,但用户手动旋转地图时,箭头没法继续保持和道路的对齐状态,需要修改代码实现这一功能。

修改方案

核心是要同时考虑设备朝向和地图相机的旋转角度,两者结合才能让箭头在用户旋转地图时依然对准道路方向。具体修改步骤如下:

  • 新增状态变量监听地图相机的朝向变化,用户旋转地图时相机的heading值会更新
  • 调整箭头旋转角度的计算逻辑,把地图相机的朝向纳入计算,抵消地图旋转带来的视角偏移
  • 优化角度计算的归一化处理,避免出现超出0-360度的异常旋转

修改后完整代码

import SwiftUI
import CoreLocation
import MapKit

struct ContentView: View {
    
    let annotations = [
        MapAnnotation(title: "Allen Rd N", coordinate: CLLocationCoordinate2D(latitude: 43.721484, longitude: -79.446093), course: 340)
    ]

    @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic)
    // 新增:存储当前地图相机的朝向角度
    @State private var cameraHeading: Double = 0
    
    @StateObject private var locationManager = LocationManager()
    
    var body: some View {
        
        VStack {
            Map(position: $position) {
                UserAnnotation()
  
                ForEach(annotations) { annotation in
                    Annotation(annotation.title, coordinate: annotation.coordinate, anchor: .bottom) {
                        // 传递设备朝向和相机朝向给标注视图
                        AnnotationView(annotation: annotation, deviceHeading: locationManager.trueNorthOffset, cameraHeading: cameraHeading)
                    }
                    .annotationTitles(.hidden)
                }
            }
            .mapStyle(.standard)
            .mapControls {
                MapUserLocationButton()
                MapCompass()
            }
            // 监听相机位置变化,实时更新相机朝向
            .onChange(of: position) { newPosition in
                guard case .camera(let camera) = newPosition else { return }
                cameraHeading = camera.heading ?? 0
            }

        }
        .padding()
    }
}


struct MapAnnotation: Identifiable {
    let id = UUID()
    let title: String
    let coordinate: CLLocationCoordinate2D
    let course: Double

    static func == (lhs: MapAnnotation, rhs: MapAnnotation) -> Bool {
        return lhs.id == rhs.id
    }
}

final class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {

    @Published var trueNorthOffset: Double = 0
    
    private let locationManager = CLLocationManager()
   
    override init() {
        super.init()
        
        locationManager.delegate = self
        checkLocationAuthorizationStatus()
        
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.startUpdatingLocation()
        locationManager.startUpdatingHeading() 
    }

    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        return
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        return
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
        self.trueNorthOffset = newHeading.trueHeading
    }

    func checkLocationAuthorizationStatus() {
        let authorizationStatus = locationManager.authorizationStatus
        
        switch authorizationStatus {
        case .notDetermined:
            locationManager.requestWhenInUseAuthorization()
            break
        case .restricted, .denied:
            break
        case .authorizedAlways, .authorizedWhenInUse:
            break
        @unknown default:
            break
        }
    }

}

struct AnnotationView: View {

    var annotation: MapAnnotation
    var deviceHeading: Double
    var cameraHeading: Double
    
    // 计算箭头最终旋转角度:道路朝向 - 设备朝向 + 相机朝向(抵消地图旋转)
    private var rotationAngle: Double {
        let angle = annotation.course - deviceHeading + cameraHeading
        // 确保角度在0-360度范围内,避免异常旋转
        return angle.truncatingRemainder(dividingBy: 360)
    }
    
    var body: some View {
        
        Image(systemName: "arrow.up.circle.fill")
            .font(.system(size: 50, weight: .bold))
            .foregroundStyle(Color.black)
            .rotationEffect(Angle(degrees: rotationAngle))
    }
        
}

关键逻辑说明

  1. 相机朝向监听:通过onChange(of: position)捕捉地图相机的变化,提取相机的heading值,这个值代表地图当前相对于正北的旋转角度。
  2. 旋转角度计算:
    • 原有逻辑annotation.course - deviceHeading保证设备旋转时箭头对齐道路
    • 新增+ cameraHeading用来抵消用户旋转地图带来的视角偏移,让箭头始终指向道路的实际地理方向
  3. 角度归一化:用truncatingRemainder(dividingBy: 360)把角度限制在0-360度范围内,避免出现连续旋转的异常情况。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 01:09:57