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

SwiftUI集成MapKit与CoreLocation时地图区域更新失效问题

问题描述

我重新发布昨日提交的问题,本次补充了可直接复现问题的精简代码示例:
我实现了嵌入MKMapView的自定义组件MyCustomMapView,组件初始加载时默认定位到固定坐标,同时提供gotoCoordinate方法,接收CLLocationCoordinate2D类型的坐标参数后,可将地图中心导航到指定坐标点。在示例代码中,点击标注为"Click here to change map position"的红色按钮即可触发该导航操作,未接入定位能力时该功能运行完全正常。

但在应用中接入负责处理用户定位逻辑的LocationViewModel后出现异常:当用户授予定位权限后,点击按钮无法再驱动地图中心移动到目标坐标;仅当注释掉@StateObject var locationViewModel = LocationViewModel()代码行后,地图移动功能才会恢复正常。

初步排查发现,只要使用了绑定delegate的CLLocationManager,修改地图显示区域的操作就不再生效,请问这是系统框架的Bug,还是我的代码实现存在错误?

复现代码

ContentView.swift

import SwiftUI

struct ContentView: View {
    @StateObject var locationViewModel = LocationViewModel()
    var body: some View {
        switch locationViewModel.authorizationStatus {
        case .notDetermined:
            AnyView(RequestLocationView())
                .environmentObject(locationViewModel)
        case .restricted:
            ErrorView(errorText: "Location use is restricted.")
        case .denied:
            ErrorView(errorText: "The app does not have location permissions. Please enable them in settings.")
        default:
            EmptyView()
        }
        
        GeometryReader { geometry in
            
            DisplayMapView(size:geometry.size)
        }
    }
}

MyCustomMapView.swift

import SwiftUI
import CoreLocation
import MapKit

struct MyCustomMapView: UIViewRepresentable {
    var map =  MKMapView()   // << constructor contract !!
    let coordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:31,longitude: -86 )
    func makeUIView(context: Context) -> MKMapView {
        map.delegate = context.coordinator
        map.showsUserLocation = true
        map.showsCompass = true
 
        let region  = MKCoordinateRegion(
            center: CLLocationCoordinate2D(latitude: coordinate.latitude,longitude: coordinate.longitude),
            span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
        map.setRegion(region, animated: true)
        return map
    }
    
    func gotoCoordinate(_ newCoordinate: CLLocationCoordinate2D ){
        let region = MKCoordinateRegion(center: newCoordinate, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
        map.setRegion(region, animated: true)
    }
      
    
    func updateUIView(_ uiView: MKMapView, context: Context) {
        
    }
    
     
    func makeCoordinator() -> MyCustomMapView.Coordinator {
        return MyCustomMapView.Coordinator(parent1: self)
    }
    
    final class Coordinator: NSObject, MKMapViewDelegate {
        var parent:MyCustomMapView
        init(parent1:MyCustomMapView){
            parent = parent1
        }
    }//class Coordinator
    
    
}

DisplayMapView.swift

import SwiftUI
import CoreLocation
import MapKit
struct DisplayMapView: View {
    @Environment(\.presentationMode) var presentationMode
    var size: CGSize

    var startCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:40.741895,longitude: -73.989308)
    
    var map = MyCustomMapView()
    var body: some View {
        ZStack(alignment:.top){
            map
             
            VStack(alignment:.leading){
                HStack {
         
                        HStack {
                            Text("Click here to change map position")
                                .onTapGesture(){
                                    map.gotoCoordinate(startCoordinate)
                                }
                            
                             
                        }
                        .padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
                        .foregroundColor(.black)
                        .background(Color(.red))
                        .cornerRadius(10.0)
                 
                }
            }.padding(.top,50).padding(.leading,20).padding(.trailing,20)
        }.ignoresSafeArea()

    }
}

LocationViewModel.swift

import Foundation
import SwiftUI
import CoreLocation

class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
    @Published var authorizationStatus: CLAuthorizationStatus
    @Published var lastSeenLocation: CLLocation?
    @Published var currentPlacemark: CLPlacemark?
    
    private let locationManager: CLLocationManager
    static let shared = LocationViewModel()
    
    override init() {
        locationManager = CLLocationManager()
        authorizationStatus = locationManager.authorizationStatus
        
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.distanceFilter = 0.4
        locationManager.startUpdatingLocation()
    }
    
    func requestPermission() {
        locationManager.requestWhenInUseAuthorization()
    }
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        authorizationStatus = manager.authorizationStatus
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        lastSeenLocation = locations.first
    }
    
    
}

struct RequestLocationView: View {
    @EnvironmentObject var locationViewModel: LocationViewModel
    
    var body: some View {
        VStack(spacing:50) {
            Image(systemName: "location.circle")
                .resizable()
                .frame(width: 100, height: 100, alignment: .center)
                .foregroundColor(Color.init(red: 0.258, green: 0.442, blue: 0.254))
            Button(action: {
                locationViewModel.requestPermission()
            }, label: {
                Label(LocalizedStringKey("allowLocationAccess"), systemImage: "location")
            })
            .padding(10)
            .foregroundColor(.white)
            .background(.green)
            .clipShape(RoundedRectangle(cornerRadius: 8))
            Text("We need your permission to give you the best experience.")
                .foregroundColor(.gray)
                .font(.caption)
        }
    }
}

struct ErrorView: View {
    var errorText: String
    
    var body: some View {
        VStack {
            Image(systemName: "xmark.octagon")
                .resizable()
                .frame(width: 100, height: 100, alignment: .center)
            Text(errorText)
        }
        .padding()
        .foregroundColor(.white)
        .background(Color.red)
        .clipShape(RoundedRectangle(cornerRadius: 12))
    }
}
问题原因

这不是系统框架Bug,是代码实现存在多个SwiftUI与UIKit混用时的典型错误:

  • UIViewRepresentable的持有方式错误:MyCustomMapView结构体内部直接创建持有MKMapView实例,又在DisplayMapView里把MyCustomMapView作为普通属性创建。SwiftUI的View是值类型,每次状态更新(比如定位权限变更、位置更新触发@Published属性刷新)时,整个ContentView和子视图都会重新计算重建,DisplayMapView里的map属性会生成新的MyCustomMapView实例,点击按钮调用的gotoCoordinate方法,操作的是已经被替换掉的旧地图实例,自然不会有效果。
  • showsUserLocation = true触发的地图自动追踪冲突:开启显示用户位置后,MKMapView默认会在位置更新时自动将视角移动到用户位置,代码中没有修改地图的userTrackingMode,手动设置区域的操作会被后续到来的位置更新自动覆盖。
  • gotoCoordinate的调用逻辑不符合UIViewRepresentable的设计规范:UIViewRepresentable的所有UI更新逻辑都应该通过updateUIView方法驱动,不应该直接在结构体上暴露方法调用内部持有的UIKit实例,值类型复制后会导致方法调用的目标和实际渲染的UI实例不一致。

额外问题:LocationViewModel中同时写了static let shared = LocationViewModel()单例和@StateObject声明,@StateObject会自动管理实例生命周期,多余的单例容易引发实例复用混乱。

修复方案

按照SwiftUI的设计规范调整代码即可:

  • 不要在UIViewRepresentable结构体内部持有MKMapView实例,MKMapView的创建、持有完全交给makeUIView生命周期管理
  • 用@Binding传递需要跳转的目标坐标,在updateUIView中监听坐标变化,操作传入的uiView实例更新地图区域
  • 手动设置地图的用户追踪模式为.none,避免自动追踪覆盖手动设置的区域
  • 把MyCustomMapView作为标准SwiftUI视图使用,不要在父视图中把它当普通类实例持有调用方法
  • 删除LocationViewModel中多余的单例声明

修复后的MyCustomMapView代码

struct MyCustomMapView: UIViewRepresentable {
    // 用Binding接收外部传入的目标坐标
    @Binding var targetCoordinate: CLLocationCoordinate2D
    // 默认初始坐标
    let defaultCoordinate = CLLocationCoordinate2D(latitude: 31, longitude: -86)
    
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        mapView.showsUserLocation = true
        mapView.showsCompass = true
        // 关闭自动追踪用户位置,避免覆盖手动移动的区域
        mapView.userTrackingMode = .none
        // 初始化默认区域
        let region = MKCoordinateRegion(
            center: defaultCoordinate,
            span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
        )
        mapView.setRegion(region, animated: true)
        return mapView
    }
    
    func updateUIView(_ uiView: MKMapView, context: Context) {
        // 当目标坐标变化时,更新地图区域
        let targetRegion = MKCoordinateRegion(
            center: targetCoordinate,
            span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
        )
        uiView.setRegion(targetRegion, animated: true)
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    
    class Coordinator: NSObject, MKMapViewDelegate {}
}

修复后的DisplayMapView代码

struct DisplayMapView: View {
    @Environment(\.presentationMode) var presentationMode
    var size: CGSize
    // 用@State管理需要跳转的目标坐标
    @State private var targetCoordinate = CLLocationCoordinate2D(latitude: 31, longitude: -86)
    // 按钮点击后要跳转到的坐标
    let jumpCoordinate = CLLocationCoordinate2D(latitude:40.741895,longitude: -73.989308)
    
    var body: some View {
        ZStack(alignment:.top){
            // 传入绑定,不需要自己持有MyCustomMapView实例
            MyCustomMapView(targetCoordinate: $targetCoordinate)
             
            VStack(alignment:.leading){
                HStack {
                        HStack {
                            Text("Click here to change map position")
                                .onTapGesture {
                                    // 修改状态即可驱动地图更新
                                    targetCoordinate = jumpCoordinate
                                }
                        }
                        .padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
                        .foregroundColor(.black)
                        .background(Color(.red))
                        .cornerRadius(10.0)
                }
            }.padding(.top,50).padding(.leading,20).padding(.trailing,20)
        }.ignoresSafeArea()
    }
}

内容的提问来源于stack exchange,提问作者Peter Van de Put

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 23:36:07