Swift UI中如何判断地图标注是否在屏幕中心并调整尺寸?
SwiftUI Map 自定义标注:中心区域显示标签与缩放适配
问题描述
我有自定义地图标注,希望当它们位于屏幕中心附近时显示标签,但不清楚如何计算地图标注在Map View上的物理位置。用ScrollView和GeometryReader实现这点很简单,但该如何在Map View中操作?
我尝试过一种需要使用MKMapView视图控制器的解决方案,但无法找到在MKMapView视图控制器中使用自定义标注的方法。此外,我还希望计算地图的缩放级别或比例来修改标注的尺寸,恳请提供相关指导。
原始代码
import SwiftUI import MapKit struct LocationsView: View { @EnvironmentObject private var vm: LocationsViewModel @State var scale: CGFloat = 0.0 var body: some View { ZStack { mapLayer.ignoresSafeArea() } } } extension LocationsView { private var mapLayer: some View { Map(coordinateRegion: $vm.mapRegion, annotationItems: vm.locations, annotationContent: { location in MapAnnotation(coordinate: location.coordinates) { Circle().foregroundStyle(.red).frame(width: 50, height: 50) .onTapGesture { vm.showNextLocation(location: location) } } }) } } class LocationsViewModel: ObservableObject { @Published var locations: [LocationMap] @Published var mapLocation: LocationMap { didSet { updateMapRegion(location: mapLocation) } } @Published var mapRegion: MKCoordinateRegion = MKCoordinateRegion() let mapSpan = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1) init() { let locations = LocationsDataService.locations self.locations = locations self.mapLocation = locations.first! self.updateMapRegion(location: locations.first!) } private func updateMapRegion(location: LocationMap) { withAnimation(.easeInOut) { mapRegion = MKCoordinateRegion( center: location.coordinates, span: mapSpan) } } func showNextLocation(location: LocationMap) { withAnimation(.easeInOut) { mapLocation = location } } } struct LocationMap: Identifiable { var id: String = UUID().uuidString let name: String let cityName: String let coordinates: CLLocationCoordinate2D } class LocationsDataService { static let locations: [LocationMap] = [ LocationMap(name: "Colosseum", cityName: "Rome", coordinates: CLLocationCoordinate2D(latitude: 41.8902, longitude: 12.4922)), LocationMap(name: "Pantheon", cityName: "Rome", coordinates: CLLocationCoordinate2D(latitude: 41.8986, longitude: 12.4769)), LocationMap(name: "Trevi Fountain", cityName: "Rome", coordinates: CLLocationCoordinate2D(latitude: 41.9009, longitude: 12.4833)) ] } struct SwiftfulMapAppApp: View { @StateObject private var vm = LocationsViewModel() var body: some View { VStack { LocationsView().environmentObject(vm) } } } #Preview(body: { SwiftfulMapAppApp() })
解决方案
1. 添加坐标转换工具与缩放级别计算
创建一个隐藏的MKMapView包装器,用于将地理坐标转换为屏幕坐标,并扩展MKMapView计算缩放级别:
// 同步地图区域并计算标注屏幕位置的工具 struct MapCoordinateConverter: UIViewRepresentable { @Binding var region: MKCoordinateRegion @Binding var annotationScreenPositions: [String: CGPoint] let annotations: [LocationMap] func makeUIView(context: Context) -> MKMapView { let mapView = MKMapView() mapView.delegate = context.coordinator mapView.isHidden = true return mapView } func updateUIView(_ uiView: MKMapView, context: Context) { if uiView.region != region { uiView.setRegion(region, animated: false) } updateAnnotationPositions(in: uiView) } func makeCoordinator() -> Coordinator { Coordinator(self) } private func updateAnnotationPositions(in mapView: MKMapView) { var positions: [String: CGPoint] = [:] annotations.forEach { location in let point = mapView.convert(location.coordinates, toPointTo: mapView) positions[location.id] = point } annotationScreenPositions = positions } class Coordinator: NSObject, MKMapViewDelegate { var parent: MapCoordinateConverter init(_ parent: MapCoordinateConverter) { self.parent = parent } func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { parent.region = mapView.region parent.updateAnnotationPositions(in: mapView) } } } // 计算MKMapView缩放级别 extension MKMapView { var zoomLevel: Double { let zoomScale = visibleMapRect.size.width / Double(bounds.size.width) return log2(zoomScale) } } // 辅助计算两点距离 extension CGPoint { func distance(to point: CGPoint) -> CGFloat { sqrt(pow(x - point.x, 2) + pow(y - point.y, 2)) } } extension CGRect { var center: CGPoint { CGPoint(x: midX, y: midY) } }
2. 修改LocationsView实现核心功能
更新LocationsView,添加标签显示逻辑和缩放适配:
struct LocationsView: View { @EnvironmentObject private var vm: LocationsViewModel @State private var annotationScreenPositions: [String: CGPoint] = [:] @State private var mapZoomLevel: Double = 0 // 中心区域阈值:屏幕中心±100pt内显示标签 private let centerThreshold: CGFloat = 100 var body: some View { ZStack { // 原有SwiftUI Map组件,适配缩放级别调整标注尺寸 Map(coordinateRegion: $vm.mapRegion, annotationItems: vm.locations, annotationContent: { location in MapAnnotation(coordinate: location.coordinates) { // 缩放级别越高(地图越近),标注越大 let annotationSize = 30 + (mapZoomLevel - 10) * 2 Circle() .foregroundStyle(.red) .frame(width: annotationSize, height: annotationSize) .onTapGesture { vm.showNextLocation(location: location) } } }) .overlay( // 隐藏的坐标转换器,实时同步标注位置 MapCoordinateConverter( region: $vm.mapRegion, annotationScreenPositions: $annotationScreenPositions, annotations: vm.locations ) .onChange(of: vm.mapRegion) { _ in // 更新当前地图缩放级别 let tempMap = MKMapView() tempMap.region = vm.mapRegion mapZoomLevel = tempMap.zoomLevel } ) // 显示中心区域内的标注标签 ForEach(vm.locations) { location in if let position = annotationScreenPositions[location.id] { let screenCenter = UIScreen.main.bounds.center let distanceToCenter = position.distance(to: screenCenter) if distanceToCenter <= centerThreshold { Text(location.name) .font(.caption) .padding(4) .background(Color.white.opacity(0.9)) .cornerRadius(4) .position(position) .offset(y: -40) // 标签置于标注上方 } } } } .ignoresSafeArea() } }
关键说明
- 坐标转换:通过隐藏的
MKMapView完成地理坐标到屏幕坐标的转换,避免直接操作SwiftUIMap的底层视图。 - 中心区域判断:计算标注与屏幕中心的距离,小于阈值时显示自定义标签。
- 缩放适配:根据
MKMapView的缩放级别动态调整标注尺寸,保证不同缩放比例下标注的可视性。 - 代码兼容:完全保留原有SwiftUI
Map和自定义标注的结构,无需重构为纯UIKit实现。
内容的提问来源于stack exchange,提问作者Ahmed Zaidan
相关产品推荐
相关产品推荐

