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

SwiftUI Map以用户位置为中心时状态切换卡顿问题求助

问题:SwiftUI/UIKit嵌入Map视图时的卡顿现象

前置要求

要让应用在地图上显示用户位置,需在项目Target的Info中开启位置权限申请:
开启iOS应用位置权限申请

卡顿场景

允许位置权限后,点击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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 17:27:02