Etiqueta: CoreLocation

  • Testing an iOS Location Manager

    Testing an iOS Location Manager

    This post explains how to validate hardware-dependent components like the LocationManager, which relies on GPS hardware. Testing such managers, including LocationManager and VideoManager, is crucial for addressing challenges developers face, such as hardware constraints, environmental variability, and simulator limitations. By mastering these techniques, you can ensure robust and reliable application behavior in real-world scenarios.

    I will guide you through the process of validating a LocationManager, introduce its test support structures, and provide examples of unit tests. Along the way, we’ll explore key techniques like mocking system services, dependency injection, and efficient testing strategies for simulators and real devices.

    This improved version enhances clarity, reduces redundancy, and improves flow while retaining all the critical details. Let me know if you’d like further refinements!

    Location Manager

    In this case, we have a location manager to handle geographic data efficiently and ensure accurate location tracking.

    import Foundation
    import CoreLocation
    
    @globalActor
    actor GlobalManager {
        static var shared = GlobalManager()
    }
    
    @GlobalManager
    class LocationManager: NSObject, ObservableObject  {
        private var clLocationManager: CLLocationManager? = nil
    
        @MainActor
        @Published var permissionGranted: Bool = false
        private var internalPermissionGranted: Bool = false {
             didSet {
                Task { [internalPermissionGranted] in
                    await MainActor.run {
                        self.permissionGranted = internalPermissionGranted
                    }
                }
            }
        }
        
        @MainActor
        @Published var speed: Double = 0.0
        private var internalSpeed: Double = 0.0 {
             didSet {
                Task { [internalSpeed] in
                    await MainActor.run {
                        self.speed = internalSpeed
                    }
                }
            }
        }
        
        init(clLocationManager: CLLocationManager = CLLocationManager()) {
            super.init()
            self.clLocationManager = clLocationManager
            clLocationManager.delegate = self
        }
        
        func checkPermission() {
            clLocationManager?.requestWhenInUseAuthorization()
        }
    }
    
    extension LocationManager: @preconcurrency CLLocationManagerDelegate {
        
        func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
            let statuses: [CLAuthorizationStatus] = [.authorizedWhenInUse, .authorizedAlways]
            if statuses.contains(status) {
                internalPermissionGranted = true
                Task {
                    internalStartUpdatingLocation()
                }
            } else if status == .notDetermined {
                checkPermission()
            } else {
                internalPermissionGranted = false
            }
        }
        
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            guard let location = locations.last else { return }
            internalSpeed = location.speed
        }
        
        private func internalStartUpdatingLocation() {
            guard CLLocationManager.locationServicesEnabled() else { return }
            clLocationManager?.startUpdatingLocation()
        }
    }
    
    This Swift code defines a LocationManager class that manages location permissions and tracking, integrating with SwiftUI’s reactive model. It uses CLLocationManager to handle location updates and authorization, updating @Published properties like permissionGranted and speed for UI binding. The class leverages Swift’s concurrency features, including @MainActor and @globalActor, to ensure thread-safe updates to the UI on the main thread. Private properties (internalPermissionGranted and internalSpeed) encapsulate internal state, while public @Published properties notify views of changes. By conforming to CLLocationManagerDelegate, it handles permission requests, starts location updates, and updates speed in response to location changes, ensuring a clean, reactive, and thread-safe integration with SwiftUI.

    Location Manager

    The key is to mock CLLocationManager and override its methods to suit the needs of your tests:

    class LocationManagerMock: CLLocationManager {
        var clAuthorizationStatus: CLAuthorizationStatus = .notDetermined
        
        override func requestWhenInUseAuthorization() {
            delegate?.locationManager!(self, didChangeAuthorization: clAuthorizationStatus)
        }
        
        override func startUpdatingLocation() {
            let sampleLocation = CLLocation(
                coordinate: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
                altitude: 10.0,
                horizontalAccuracy: 5.0,
                verticalAccuracy: 5.0,
                course: 90.0,
                speed: 10.0,
                timestamp: Date()
            )
            delegate?.locationManager!(self, didUpdateLocations: [sampleLocation])
        }
    }

    For our test purposes, we are validating the location-granted request service and starting the location update process. During permission validation, we use an attribute to provide the desired response when requestWhenInUseAuthorization is executed. Additionally, we include a sample CLLocation to simulate the location data when startUpdatingLocation is called.

    To ensure robust validation of authorization, we have implemented the following unit tests:

        @Test func testAthorizacionRequestDenied() async throws {
            let locationManagerMock = LocationManagerMock()
            locationManagerMock.clAuthorizationStatus = .denied
            let sut = await LocationManager(clLocationManager: locationManagerMock)
            await sut.checkPermission()
            // Wait for the @Published speed property to update
            try await Task.sleep(nanoseconds: 1_000_000)
            await #expect(sut.permissionGranted == false)
        }
    
    
        @Test func testAthorizacionRequestAuthorized() async throws {
            let locationManagerMock = LocationManagerMock()
            locationManagerMock.clAuthorizationStatus =  .authorizedWhenInUse
            let sut = await LocationManager(clLocationManager: locationManagerMock)
            await sut.checkPermission()
            // Wait for the @Published speed property to update
            try await Task.sleep(nanoseconds: 1_000_000)
            await #expect(sut.permissionGranted == true)
        }
    Validates scenarios where the user grants or denies location services authorization. Also validates location updates.
        @Test func testStartUpdatingLocation() async throws {
            let locationManagerMock = LocationManagerMock()
            locationManagerMock.clAuthorizationStatus =  .authorizedWhenInUse
            let sut = await LocationManager(clLocationManager: locationManagerMock)
            await sut.checkPermission()
            // Wait for the @Published speed property to update
            try await Task.sleep(nanoseconds: 50_000_000)
                   
            await #expect(sut.speed == 10.00)
        }

    Basically we check location speed.

    Conclusions

    In this post, I have presented a method for validating hardware-dependent issues, such as GPS information. You can find the source code used for this post in the repository linked below.

  • iOS Location Managers: A Thread-Safe Approach

    iOS Location Managers: A Thread-Safe Approach

    The aim of this post is just to explain how to migrate any app that uses CoreLocation to Swift 6.0. First step will be create a simple app that presents current location and later on we will close the post with the migration.

    CoreLocation

    Core Location Framework Overview

    Core Location is an iOS framework that enables apps to access and utilize a device’s geographic location, altitude, and orientation. It provides robust services for location-based functionalities, leveraging device components such as Wi-Fi, GPS, Bluetooth, cellular hardware, and other sensors.

    Key Functionalities of Core Location:

    1. Location Services:
      • Standard Location Service: Tracks user location changes with configurable accuracy.
      • Significant Location Service: Provides updates for significant location changes.
    2. Regional Monitoring: Monitors entry and exit events for specific geographic regions.
    3. Beacon Ranging: Detects and tracks nearby iBeacon devices.
    4. Visit Monitoring: Identifies locations where users spend significant periods of time.
    5. Compass Headings: Tracks the user’s directional heading.
    6. Altitude Information: Supplies data about the device’s altitude.
    7. Geofencing: Enables the creation of virtual boundaries that trigger notifications upon entry or exit.

    iOS Location sample app:

    Create a new blank iOS SwiftUI APP.

    This Swift code defines a class named LocationManager that integrates with Apple’s Core Location framework to handle location-related tasks such as obtaining the user’s current coordinates and resolving the corresponding address. Below is a breakdown of what each part of the code does

    import Foundation
    import CoreLocation
    
    class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
        
        static let shared = LocationManager()
        
        private var locationManager = CLLocationManager()
        private let geocoder = CLGeocoder()
        
        @Published var currentLocation: CLLocationCoordinate2D?
        @Published var currentAddress: CLPlacemark?
        
        private override init() {
            super.init()
            locationManager.delegate = self
            locationManager.desiredAccuracy = kCLLocationAccuracyBest
        }
        
        func checkAuthorization() {
            switch locationManager.authorizationStatus {
            case .notDetermined:
                locationManager.requestWhenInUseAuthorization()
            case .restricted, .denied:
                print("Location access denied")
            case .authorizedWhenInUse, .authorizedAlways:
                locationManager.requestLocation()
            @unknown default:
                break
            }
        }
        
        func requestLocation() {
            locationManager.requestLocation()
        }
        
        func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
            print("Failed to find user's location: \(error.localizedDescription)")
        }
        
        func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
            checkAuthorization()
        }
    
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            if let location = locations.first {
                self.currentLocation = CLLocationCoordinate2D(latitude: location.coordinate.latitude,
                                                              longitude: location.coordinate.longitude)
                reverseGeocode(location: location)
            }
        }
        
        private func reverseGeocode(location: CLLocation) {
            geocoder.reverseGeocodeLocation(location) { [weak self] placemarks, error in
                if let placemark = placemarks?.first, error == nil {
                    self?.currentAddress = CLPlacemark(placemark: placemark)
                } else {
                    print("Error during reverse geocoding: \(error?.localizedDescription ?? "Unknown error")")
                }
            }
        }
    }
    

    Main Responsabilites and features:

    1. Singleton Pattern

      • The class uses a shared instance (LocationManager.shared) to provide a global access point.
      • The initializer (private override init()) is private to enforce a single instance.
    1. CoreLocation Setup

      • CLLocationManager: Manages location-related activities (e.g., obtaining current location, monitoring location updates).
      • CLGeocoder: Converts geographic coordinates to human-readable addresses (reverse geocoding).
    2. Published Properties

      • @Published: Allows properties (currentLocation and currentAddress) to trigger UI updates in SwiftUI whenever they change.
    3. Authorization Handling

      • Checks and requests location permissions (checkAuthorization()).
      • Responds to changes in authorization status (locationManagerDidChangeAuthorization).
    4. Requesting Location

      • requestLocation(): Asks CLLocationManager to fetch the current location.
    5. Delegate Methods

      • Handles success (didUpdateLocations) and failure (didFailWithError) when fetching the location.
      • Updates currentLocation with the retrieved coordinates.
      • Performs reverse geocoding to convert coordinates to a readable address (reverseGeocode(location:)).
    6. Reverse Geocoding

      • Converts a CLLocation into a CLPlacemark (e.g., city, street, country).
      • Updates currentAddress on success or logs an error if reverse geocoding fails.

    How it Works in Steps

    1. Initialization

      • The singleton instance is created (LocationManager.shared).
      • CLLocationManager is set up with a delegate (self) and a desired accuracy.
    2. Authorization

      • The app checks location permissions using checkAuthorization().
      • If permission is undetermined, it requests authorization (requestWhenInUseAuthorization()).
      • If authorized, it requests the user’s current location (requestLocation()).
    3. Location Fetch

      • When a location update is received, didUpdateLocations processes the first location in the array.
      • The geographic coordinates are stored in currentLocation.
      • The reverseGeocode(location:) method converts the location to an address (currentAddress).
    4. Error Handling

      • Location fetch errors are logged via didFailWithError.
      • Reverse geocoding errors are logged in reverseGeocode.

    Finally we’re are going to request some location data from content view:

    struct ContentView: View {
        @StateObject private var locationManager = LocationManager()
        
        var body: some View {
            VStack(spacing: 20) {
                if let location = locationManager.currentLocation {
                    Text("Latitude: \(location.latitude)")
                    Text("Longitude: \(location.longitude)")
                } else {
                    Text("Location not available")
                }
                
                if let address = locationManager.currentAddress {
                    Text("Name: \(address.name ?? "Unknown")")
                    Text("Town: \(address.locality ?? "Unknown")")
                    Text("Country: \(address.country ?? "Unknown")")
                } else {
                    Text("Address not available")
                }
                
                Button(action: {
                    locationManager.requestLocation()
                }) {
                    Text("Request Location")
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                }
            }
            .onAppear {
                locationManager.checkAuthorization()
            }
            .padding()
        }
    }

    Last but not least be sure that ContentView is executing the view that we have just created. And be sure that you have a description for NSLocationWhenInUseUsageDescription setting.

    To run the app, ensure it is deployed on a real device (iPhone or iPad). When the app prompts for permission to use location services, make sure to select «Allow.»

    …Thread safe approach

    This is the Side B of the post—or in other words, the part where we save the thread! 😄 Now, head over to the project settings and set Strict Concurrency Checking to Complete.

    … and Swift language version to Swift 6.

    The first issue we identified is that the LocationManager is a singleton. This design allows it to be accessed from both isolated domains and non-isolated domains.

    In this case, most of the helper methods are being called directly from views, so it makes sense to move this class to @MainActor.

    @MainActor
    class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
     

    Now is the time to examine the data returned in the delegate methods. Our delegate methods do not modify the data, but some of them forward a copy of the received data. With our current implementation, this ensures that we avoid data races.

    In computer science, there are no «silver bullets,» and resolving issues when migrating from Swift 6 is no exception. When reviewing library documentation, if it is available, you should identify the specific domain or context from which the library provides its data. For Core Location, for instance, ensure that the CLLocationManager operates on the same thread on which it was initialized.

    We have a minimum set of guarantees to establish the protocol as @preconcurrency.

    @MainActor
    class LocationManager: NSObject, ObservableObject, @preconcurrency CLLocationManagerDelegate {
     

    At this point, we fulfill the Swift 6 strict concurrency check requirements. By marking the singleton variable as @MainActor, we fix both of the previous issues at once.

    class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
       
        @MainActor
        static let shared = LocationManager()
     

    Fixing migration issues is an iterative task. The more you work on it, the faster you can find a solution, but sometimes there is no direct fix. Build and deploy on a real device to ensure everything is working as expected.

    You can find the source code for this post in this repository.

    Conclusions

    In this post, you have seen how easy is to migrate CoreLocationManager

    References