Position is a Swift 6-ready, actor-based location positioning library for iOS and macOS with modern async/await APIs and AsyncSequence support.
Swift Platforms Swift Package Manager License
| Feature | Description |
|---|---|
| π | Swift 6 - Full concurrency support with actor isolation |
| β‘ | Modern async/await - One-shot location requests with async/await |
| π | AsyncSequence - Reactive updates for location, heading, and authorization |
| π | Actor-based - Thread-safe by design with Swift concurrency |
| π | Customizable location accuracy and filtering |
| π | Distance and time-based location filtering |
| π‘ | Continuous location tracking with configurable accuracy |
| π§ | Device heading and compass support (iOS only) |
| π | Permission management with async authorization requests |
| π | Geospatial utilities for distance and bearing calculations |
| π | Battery-aware location accuracy adjustments |
| π | Visit monitoring for significant location changes |
| π’ | Floor level detection in supported venues |
| π’ | Place data formatting and geocoding utilities |
| π | vCard location sharing support |
- iOS 16.0+ / macOS 13.0+
- Swift 6.0+ (also supports Swift 5 mode)
- Xcode 16.0+
Add Position to your project using Swift Package Manager:
- In Xcode, select File > Add Package Dependencies...
- Enter the repository URL:
https://github.com/piemonte/Position - Select version
1.0.0or later
Or add it to your Package.swift:
dependencies: [ .package(url: "https://github.com/piemonte/Position", from: "1.0.0") ]
Add the appropriate location usage descriptions to your app's Info.plist:
<key>NSLocationWhenInUseUsageDescription</key> <string>Your app needs location access to provide location-based features.</string> <!-- Optional: For always authorization --> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <string>Your app needs location access even in the background.</string>
import Position class LocationManager { let position = Position() func setup() async { // Check and request permissions let status = await position.locationServicesStatus switch status { case .notDetermined: let newStatus = await position.requestWhenInUseLocationAuthorization() print("Authorization result: \(newStatus)") case .allowedWhenInUse, .allowedAlways: await requestLocation() case .denied, .notAvailable: print("Location services unavailable") } } func requestLocation() async { do { // One-shot location request with async/await let location = try await position.currentLocation() print("π Location: \(location.coordinate)") // Or with custom accuracy let preciseLocation = try await position.currentLocation( desiredAccuracy: kCLLocationAccuracyBest ) print("π Precise location: \(preciseLocation.coordinate)") } catch { print("β Location error: \(error)") } } }
import Position class LocationTracker { let position = Position() func startTracking() async { // Configure tracking parameters await position.setDistanceFilter(10) // meters position.trackingDesiredAccuracyWhenActive = kCLLocationAccuracyBest // Start location updates await position.startUpdating() // Consume location updates Task { for await location in position.locationUpdates { print("π New location: \(location.coordinate)") updateUI(with: location) } } // Monitor authorization changes Task { for await status in position.authorizationUpdates { print("π Authorization changed: \(status)") handleAuthorizationChange(status) } } // Track heading updates (iOS only) Task { for await heading in position.headingUpdates { print("π§ Heading: \(heading.trueHeading)Β°") } } } func stopTracking() async { await position.stopUpdating() } }
import SwiftUI import Position @MainActor class LocationViewModel: ObservableObject { @Published var currentLocation: CLLocation? @Published var authorizationStatus: Position.LocationAuthorizationStatus = .notDetermined private let position = Position() private var locationTask: Task<Void, Never>? func startLocationUpdates() { locationTask = Task { await position.startUpdating() for await location in position.locationUpdates { currentLocation = location } } } func stopLocationUpdates() async { locationTask?.cancel() await position.stopUpdating() } } struct LocationView: View { @StateObject private var viewModel = LocationViewModel() var body: some View { VStack { if let location = viewModel.currentLocation { Text("π \(location.coordinate.latitude), \(location.coordinate.longitude)") } else { Text("No location available") } } .task { viewModel.startLocationUpdates() } } }
let position = Position() // Location updates for await location in position.locationUpdates { print("Location: \(location)") } // Heading updates (iOS only) for await heading in position.headingUpdates { print("Heading: \(heading)") } // Authorization status changes for await status in position.authorizationUpdates { print("Auth status: \(status)") } // Floor changes (when available) for await floor in position.floorUpdates { print("Floor: \(floor.level)") } // Visit monitoring for await visit in position.visitUpdates { print("Visit at: \(visit.coordinate)") } // Error handling for await error in position.errorUpdates { print("Location error: \(error)") }
import Position import CoreLocation let location1 = CLLocation(latitude: 37.7749, longitude: -122.4194) // San Francisco let location2 = CLLocation(latitude: 34.0522, longitude: -118.2437) // Los Angeles // Calculate distance let distance = location1.distance(from: location2) print("Distance: \(distance / 1000) km") // Or use Measurement API for type-safe conversions let measurement = Measurement(value: distance, unit: UnitLength.meters) let km = measurement.converted(to: .kilometers).value print("Distance: \(km) km") // Calculate bearing let bearing = location1.bearing(toLocation: location2) print("Bearing: \(bearing)Β°") // Calculate coordinate at bearing and distance let coordinate = location1.locationCoordinate(withBearing: 45, distanceMeters: 1000) print("New coordinate: \(coordinate)") // Pretty distance description (localized) let description = location1.prettyDistanceDescription(fromLocation: location2) print("Distance: \(description)")
let position = Position() // Enable automatic battery management await position.setAdjustLocationUseFromBatteryLevel(true) // Manual accuracy adjustment based on app state await position.setAdjustLocationUseWhenBackgrounded(true) // Configure accuracy levels position.trackingDesiredAccuracyWhenActive = kCLLocationAccuracyBest position.trackingDesiredAccuracyWhenInBackground = kCLLocationAccuracyKilometer // The library will automatically adjust accuracy when: // - Battery level drops below 20% (switches to reduced accuracy) // - App enters background (uses trackingDesiredAccuracyWhenInBackground) // - App becomes active again (restores trackingDesiredAccuracyWhenActive)
import Position import CoreLocation let location = CLLocation(latitude: 37.7749, longitude: -122.4194) // Create vCard for sharing location (async version) do { let vCardURL = try await location.vCard(name: "Golden Gate Bridge") // Share the vCard file URL print("vCard created at: \(vCardURL)") } catch { print("Failed to create vCard: \(error)") }
import Position import CoreLocation // Format address components let address = CLPlacemark.shortStringFromAddressElements( address: "1 Infinite Loop", locality: "Cupertino", administrativeArea: "CA" ) print("Formatted address: \(address ?? "")") // Pretty descriptions from placemarks if let placemark = somePlacemark { // Simple pretty description let description = placemark.prettyDescription() print("Location: \(description)") // Zoom-level aware description let zoomDescription = placemark.prettyDescription(withZoomLevel: 14) print("Location at zoom 14: \(zoomDescription)") // Full address string let fullAddress = placemark.stringFromPlacemark() print("Full address: \(fullAddress ?? "")") }
-
No More Singleton
// Old Position.shared.performOneShotLocationUpdate(...) // New let position = Position() try await position.currentLocation()
-
Async/Await Instead of Callbacks
// Old Position.shared.performOneShotLocationUpdate(withDesiredAccuracy: 100) { result in switch result { case .success(let location): print(location) case .failure(let error): print(error) } } // New do { let location = try await position.currentLocation(desiredAccuracy: 100) print(location) } catch { print(error) }
-
AsyncSequence Instead of Observers
// Old Position.shared.addObserver(self) func position(_ position: Position, didUpdateTrackingLocations locations: [CLLocation]?) { // Handle update } // New for await location in position.locationUpdates { // Handle update }
-
Actor-Based API
// Most Position methods are now async await position.startUpdating() await position.stopUpdating() let status = await position.locationServicesStatus
The observer pattern is maintained but deprecated. Update your code to use AsyncSequence for future compatibility.
| Feature | iOS | macOS |
|---|---|---|
| Location Updates | β | β |
| Heading Updates | β | β |
| Visit Monitoring | β | β |
| Battery Monitoring | β | β |
| Background Updates | β | |
| Floor Detection | β | β |
- Concurrency: Position is an actor - use
awaitwhen calling its methods - Lifecycle: Create Position instances as needed, no singleton required
- AsyncSequence: Prefer AsyncSequence over deprecated observer pattern
- Error Handling: Always handle errors in location requests
- Permissions: Check authorization status before requesting location
Complete API documentation is available in the source code with comprehensive DocC comments.
We welcome contributions! Please see our Contributing Guidelines for details.
Position is available under the MIT license. See the LICENSE file for more information.