SwiftUI map lags when it's centered in the user's location and state changes

Please note that for this app to be able show you your location on the map you need to enable it to ask for permission to track the user's location, in the project's targets' info:

Here's also a video that illustrates the lag: https://youtube.com/shorts/DSl-umGxs20?feature=share.

That being said, if you run this SwiftUI app and allow it to track your location, tap the MapUserLocationButton and press the buttons at the bottom, you'll see that the map lags:

import SwiftUI
import MapKit
import CoreLocation

struct ContentView: View {
    let currentLocationManager = CurrentUserLocationManager()
    let 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: - Location related code -
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): method called redundantly: locationManager had already been initialized")
        }
    }
}; 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("Your location is restricted")
        case .denied:
            print("Go into setting to change it")
        case .authorizedAlways, .authorizedWhenInUse, .authorized:
//            locationManager.startUpdatingLocation()
            break
        @unknown default:
            break
        }
    }
}

I've also tried in a UIKit app (by just embedding ContentView in a view controller), with the same results:

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 {
    let 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()
        }
    }
} }

extension ViewController { @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): method called redundantly: locationManager had already been initialized")
        }
    }
}; 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("Your location is restricted")
        case .denied:
            print("Go into setting to change it")
        case .authorizedAlways, .authorizedWhenInUse, .authorized:
//            locationManager.startUpdatingLocation()
            break
        @unknown default:
            break
        }
    }
}

If you don't center the map in the user's location, you might see an occasiona lag, but it seems to me that this only happens once.

How do I avoid these lags altogether?

Xcode 15.4

Answered by Frameworks Engineer in 790191022

Hello,

I'm not sure if I see lag here. There location indicator is shifting based on safe area insets as described below.

The view can add 2 buttons in the safeAreaInset with the following behaviors:

  • Tapping the "First button" will exclude "First button".
  • Tapping the "Second button" will add "First button".

As the "First button" is added/removed, the safe area inset changes. You can tell this from how the Map's Legal link is moving up and down as the button state changes. The size of the safe area insets affects which part of the view the Map considers available to frame content in, so when centering on the user location, it will center the user location view in the center of the safe area.

When the "First button" is added, the safe area inset increases, and thus the safe area shrinks. As a result, there's no longer the same amount of space for the map view to center the user location in. The view re-centers the user location view within the new safe area and thus the visible region of the map adjusts.

When the "First button" is removed, the safe area inset decreases, and thus the safe area increases. As a result, the user location view is re-centered in the new available safe area.

It's expected behavior that the map view reframes when the safe area changes. If you'd like to avoid the content shifting, you'll need to ensure your safe area inset does not change. Please let us know if that's not the problem you're trying to describe.

Accepted Answer

Hello,

I'm not sure if I see lag here. There location indicator is shifting based on safe area insets as described below.

The view can add 2 buttons in the safeAreaInset with the following behaviors:

  • Tapping the "First button" will exclude "First button".
  • Tapping the "Second button" will add "First button".

As the "First button" is added/removed, the safe area inset changes. You can tell this from how the Map's Legal link is moving up and down as the button state changes. The size of the safe area insets affects which part of the view the Map considers available to frame content in, so when centering on the user location, it will center the user location view in the center of the safe area.

When the "First button" is added, the safe area inset increases, and thus the safe area shrinks. As a result, there's no longer the same amount of space for the map view to center the user location in. The view re-centers the user location view within the new safe area and thus the visible region of the map adjusts.

When the "First button" is removed, the safe area inset decreases, and thus the safe area increases. As a result, the user location view is re-centered in the new available safe area.

It's expected behavior that the map view reframes when the safe area changes. If you'd like to avoid the content shifting, you'll need to ensure your safe area inset does not change. Please let us know if that's not the problem you're trying to describe.

SwiftUI map lags when it's centered in the user's location and state changes
 
 
Q