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

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完成地理坐标到屏幕坐标的转换,避免直接操作SwiftUI Map的底层视图。
  • 中心区域判断:计算标注与屏幕中心的距离,小于阈值时显示自定义标签。
  • 缩放适配:根据MKMapView的缩放级别动态调整标注尺寸,保证不同缩放比例下标注的可视性。
  • 代码兼容:完全保留原有SwiftUI Map和自定义标注的结构,无需重构为纯UIKit实现。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 17:23:14