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

Copyright © 2024-2025 JaviOS. All rights reserved