SwiftUI Map以用户位置为中心时状态切换卡顿问题求助
问题:SwiftUI/UIKit嵌入Map视图时的卡顿现象
前置要求
要让应用在地图上显示用户位置,需在项目Target的Info中开启位置权限申请:
卡顿场景
允许位置权限后,点击MapUserLocationButton将地图中心定位到用户位置,再点击底部按钮切换shouldAllowSearches状态,地图会出现明显卡顿。该问题在纯SwiftUI应用和SwiftUI视图嵌入UIKit的场景中均复现。
复现代码
SwiftUI版本
import SwiftUI import MapKit import CoreLocation struct ContentView: View { let currentLocationManager = CurrentUserLocationManager() @State private var mapLocationsManager = MapLocationsManager() @State private var mapCameraPosition: MapCameraPosition = .automatic var body: some View { Map( position: $mapCameraPosition ) .safeAreaInset(edge: .bottom) { VStack { if mapLocationsManager.shouldAllowSearches { Button("First button") { mapLocationsManager.shouldAllowSearches = false } } Button("Second button") { mapLocationsManager.shouldAllowSearches = true } } .frame(maxWidth: .infinity) .padding() } .mapControls { MapUserLocationButton() } } } @Observable class MapLocationsManager { var shouldAllowSearches = false } // MARK: - 定位相关代码 - class CurrentUserLocationManager: NSObject { var locationManager: CLLocationManager? override init() { super.init() startIfNecessary() } func startIfNecessary() { if locationManager == nil { locationManager = .init() locationManager?.delegate = self } else { print(">> \(Self.self).\(#function): 方法重复调用:locationManager已初始化") } } } extension CurrentUserLocationManager: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { checkLocationAuthorization() } } extension CurrentUserLocationManager { private func checkLocationAuthorization() { guard let locationManager else { return } switch locationManager.authorizationStatus { case .notDetermined: locationManager.requestWhenInUseAuthorization() case .restricted: print("位置权限受限") case .denied: print("请前往设置修改位置权限") case .authorizedAlways, .authorizedWhenInUse, .authorized: break @unknown default: break } } }
UIKit嵌入SwiftUI版本
import UIKit import MapKit import SwiftUI import CoreLocation class ViewController: UIViewController { let currentLocationManager = CurrentUserLocationManager() override func viewDidLoad() { super.viewDidLoad() currentLocationManager.startIfNecessary() let hostingController = UIHostingController(rootView: MapView()) addChild(hostingController) hostingController.view.frame = view.bounds hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(hostingController.view) hostingController.didMove(toParent: self) } } extension ViewController { struct MapView: View { @State private var mapLocationsManager = MapLocationsManager() @State private var mapCameraPosition: MapCameraPosition = .automatic var body: some View { Map(position: $mapCameraPosition) .safeAreaInset(edge: .bottom) { VStack { if mapLocationsManager.shouldAllowSearches { Button("First button") { mapLocationsManager.shouldAllowSearches = false } } Button("Second button") { mapLocationsManager.shouldAllowSearches = true } } .frame(maxWidth: .infinity) .padding() } .mapControls { MapUserLocationButton() } } } @Observable class MapLocationsManager { var shouldAllowSearches = false } } class CurrentUserLocationManager: NSObject { var locationManager: CLLocationManager? func startIfNecessary() { if locationManager == nil { locationManager = .init() locationManager?.delegate = self } else { print(">> \(Self.self).\(#function): 方法重复调用:locationManager已初始化") } } } extension CurrentUserLocationManager: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { checkLocationAuthorization() } } extension CurrentUserLocationManager { private func checkLocationAuthorization() { guard let locationManager else { return } switch locationManager.authorizationStatus { case .notDetermined: locationManager.requestWhenInUseAuthorization() case .restricted: print("位置权限受限") case .denied: print("请前往设置修改位置权限") case .authorizedAlways, .authorizedWhenInUse, .authorized: break @unknown default: break } } }
补充现象
若不将地图中心定位到用户位置,卡顿可能仅偶尔出现一次。
环境
Xcode 15.4,iOS 17.5,iPhone 15 Pro模拟器
解决方案
1. 独立封装底部按钮视图,减少Map重绘范围
将底部按钮区域抽为独立子视图,避免因按钮显隐直接触发整个Map视图的重绘:
struct BottomButtonBar: View { @Bindable var manager: MapLocationsManager var body: some View { VStack { if manager.shouldAllowSearches { Button("First button") { manager.shouldAllowSearches = false } } Button("Second button") { manager.shouldAllowSearches = true } } .frame(maxWidth: .infinity) .padding() } } // 在ContentView中替换原safeAreaInset内容: .safeAreaInset(edge: .bottom) { BottomButtonBar(manager: mapLocationsManager) }
2. 手动管理地图相机位置,避免自动模式冲突
使用.region初始化相机位置而非.automatic,并通过定位回调手动更新地图中心,减少自动模式下的内部状态冲突:
// 在ContentView中修改相机初始化: @State private var mapCameraPosition: MapCameraPosition = .region(MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 37.3349, longitude: -122.0090), span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) )) // 扩展CurrentUserLocationManager添加位置更新回调: extension CurrentUserLocationManager: CLLocationManagerDelegate { // ... 原有授权回调 ... func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let latestLoc = locations.last else { return } // 可通过ObservableObject或通知将位置传递给ContentView,更新mapCameraPosition // 示例:若使用ObservableObject封装位置数据 // locationData.coordinate = latestLoc.coordinate } }
3. 简化状态管理,避免不必要的ObservableObject
直接使用@State管理shouldAllowSearches,减少中间层的状态发布开销:
// 在ContentView中替换原MapLocationsManager: @State private var shouldAllowSearches = false // 底部按钮区域修改为: VStack { if shouldAllowSearches { Button("First button") { shouldAllowSearches = false } } Button("Second button") { shouldAllowSearches = true } }
4. 手动控制用户跟踪模式,禁用自动动画冲突
通过userTrackingMode手动控制地图是否跟踪用户位置,避免自动模式下的动画与视图更新冲突:
@State private var userTrackingMode: MapUserTrackingMode = .none // 修改Map视图: Map( position: $mapCameraPosition, userTrackingMode: $userTrackingMode ) .mapControls { MapUserLocationButton() .onTapGesture { userTrackingMode = userTrackingMode == .follow ? .none : .follow } }
内容的提问来源于Stack Exchange,提问作者Filippo
相关产品推荐
相关产品推荐

