Position
is a Swift 6-ready, actor-based location positioning library for iOS and macOS with modern async/await APIs and AsyncSequence support.
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 15.0+ / macOS 11.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.0
or 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
await
when 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.