swiftuimapkit

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: enable the iOS app to ask for permission to track the user's location

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()
    @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: - 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 it 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 {
    @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()
        }
    }
} }

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 occasional lag, but it seems to me that it only happens at most once.

How do I avoid these lags altogether?

Xcode 15.4, iOS 17.5, iPhone 15 Pro simulator.


Solution

  • The user location indicator is centered in the view's safe area.

    So, since the safe area inset resizes when its content changes, you should just use a different modifier to display views above the map, such as .overlay(alignment:content:):

    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
            )
            .overlay {
                VStack {
                    Spacer()
                    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?
        
        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
            }
        }
    }
    

    Credit: https://developer.apple.com/forums//thread/756534?answerId=790191022#790191022.