Skip to content
Skip
3k

Sensors

The SkipDevice module is a dual-platform Skip framework that provides access to network reachability, location services, and device sensor data (accelerometer, gyroscope, magnetometer, and barometer).

On iOS, the module wraps CoreMotion and CoreLocation. On Android, it wraps the Sensor and Location APIs. All sensor providers expose a unified AsyncThrowingStream interface that works identically on both platforms.

To include this framework in your project, add the following dependency to your Package.swift file:

let package = Package(
name: "my-package",
products: [
.library(name: "MyProduct", targets: ["MyTarget"]),
],
dependencies: [
.package(url: "https://source.skip.dev/skip-device.git", "0.0.0"..<"2.0.0"),
],
targets: [
.target(name: "MyTarget", dependencies: [
.product(name: "SkipDevice", package: "skip-device")
])
]
)

All sensor providers follow the same pattern:

  1. Create a provider instance (retain it for the lifetime of the monitoring session)
  2. Optionally set updateInterval before calling monitor()
  3. Iterate the AsyncThrowingStream returned by monitor()
  4. The stream automatically stops when the task is cancelled or the provider is deallocated
let provider = SomeProvider()
provider.updateInterval = 0.1 // optional, in seconds
do {
for try await event in provider.monitor() {
// process event
}
} catch {
// handle error
}

Check provider.isAvailable before starting to determine if the hardware is present on the device.

Check whether the device currently has network access.

iOSAndroid
APISCNetworkReachabilityConnectivityManager
import SkipDevice
let isReachable = NetworkReachability.isNetworkReachable
PlatformRequirement
iOSNo permission required
AndroidDeclare ACCESS_NETWORK_STATE in AndroidManifest.xml

Android manifest entry:

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Access the device’s geographic location via GPS, network, and fused providers. Provides latitude, longitude, altitude, speed, course, and accuracy information.

iOSAndroid
APICLLocationManagerLocationManager (FUSED_PROVIDER)
import SkipDevice
let provider = LocationProvider()
let location = try await provider.fetchCurrentLocation()
print("lat: \(location.latitude), lon: \(location.longitude), alt: \(location.altitude)")
import SwiftUI
import SkipKit // for PermissionManager
import SkipDevice
struct LocationView: View {
@State var event: LocationEvent?
@State var errorMessage: String?
var body: some View {
VStack {
if let event = event {
Text("Latitude: \(event.latitude)")
Text("Longitude: \(event.longitude)")
Text("Altitude: \(event.altitude) m")
Text("Speed: \(event.speed) m/s")
Text("Course: \(event.course)")
Text("Accuracy: \(event.horizontalAccuracy) m")
} else if let errorMessage = errorMessage {
Text(errorMessage).foregroundStyle(.red)
} else {
ProgressView()
}
}
.task {
let status = await PermissionManager.requestLocationPermission(precise: true, always: false)
guard status.isAuthorized == true else {
errorMessage = "Location permission denied"
return
}
let provider = LocationProvider()
do {
for try await event in provider.monitor() {
self.event = event
}
} catch {
errorMessage = "\(error)"
}
}
}
}
PropertyTypeDescription
latitudeDoubleLatitude in degrees
longitudeDoubleLongitude in degrees
horizontalAccuracyDoubleHorizontal accuracy in meters
altitudeDoubleAltitude (Mean Sea Level) in meters
ellipsoidalAltitudeDoubleEllipsoidal altitude in meters
verticalAccuracyDoubleVertical accuracy in meters
speedDoubleSpeed in meters per second
speedAccuracyDoubleSpeed accuracy in meters per second
courseDoubleCourse/bearing in degrees
courseAccuracyDoubleCourse accuracy in degrees
timestampTimeIntervalEvent timestamp

Location requires both a metadata declaration and a runtime permission request on both platforms. Use SkipKit’s PermissionManager for cross-platform runtime permission handling.

PlatformRequirement
iOSDeclare NSLocationWhenInUseUsageDescription in Darwin/AppName.xcconfig
AndroidDeclare ACCESS_FINE_LOCATION and/or ACCESS_COARSE_LOCATION in AndroidManifest.xml
BothRequest permission at runtime via PermissionManager.requestLocationPermission()

iOS xcconfig entry:

INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This app uses your location to …"

Android manifest entries:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

The accelerometer, gyroscope, magnetometer, and barometer share a common iOS permission requirement and usage pattern. On Android, motion sensors do not require any runtime permissions.

PlatformRequirement
iOSDeclare NSMotionUsageDescription in Darwin/AppName.xcconfig (no runtime request needed)
AndroidNo permission required for accelerometer, gyroscope, or magnetometer. Barometer requires a <uses-feature> declaration.

iOS xcconfig entry:

INFOPLIST_KEY_NSMotionUsageDescription = "This app uses motion sensors to …"

Measures acceleration force on three axes in G’s (gravitational force units, where 1G = 9.81 m/s). At rest face-up, the device reports approximately (0, 0, -1) G.

iOSAndroid
APICMMotionManager.startAccelerometerUpdatesSensor.TYPE_ACCELEROMETER
UnitsG’sm/s (converted to G’s by SkipDevice)
import SwiftUI
import SkipDevice
struct AccelerometerView: View {
@State var event: AccelerometerEvent?
var body: some View {
VStack {
if let event = event {
Text("X: \(event.x) G")
Text("Y: \(event.y) G")
Text("Z: \(event.z) G")
}
}
.task {
let provider = AccelerometerProvider()
guard provider.isAvailable else { return }
provider.updateInterval = 0.1
do {
for try await event in provider.monitor() {
self.event = event
}
} catch {
logger.error("accelerometer error: \(error)")
}
}
}
}
PropertyTypeDescription
xDoubleX-axis acceleration in G’s
yDoubleY-axis acceleration in G’s
zDoubleZ-axis acceleration in G’s
timestampTimeIntervalEvent timestamp (seconds since boot)

Measures angular rotation rate on three axes in radians per second.

iOSAndroid
APICMMotionManager.startGyroUpdatesSensor.TYPE_GYROSCOPE
Unitsrad/srad/s
import SwiftUI
import SkipDevice
struct GyroscopeView: View {
@State var event: GyroscopeEvent?
var body: some View {
VStack {
if let event = event {
Text("X: \(event.x) rad/s")
Text("Y: \(event.y) rad/s")
Text("Z: \(event.z) rad/s")
}
}
.task {
let provider = GyroscopeProvider()
guard provider.isAvailable else { return }
provider.updateInterval = 0.1
do {
for try await event in provider.monitor() {
self.event = event
}
} catch {
logger.error("gyroscope error: \(error)")
}
}
}
}
PropertyTypeDescription
xDoubleAngular speed around the x-axis in rad/s
yDoubleAngular speed around the y-axis in rad/s
zDoubleAngular speed around the z-axis in rad/s
timestampTimeIntervalEvent timestamp (seconds since boot)

Measures the ambient magnetic field on three axes in microteslas. Returns calibrated values with device bias removed on both platforms. Useful for compass headings and magnetic field detection.

iOSAndroid
APICMDeviceMotion.magneticField (calibrated)Sensor.TYPE_MAGNETIC_FIELD (calibrated)
Unitsmicroteslasmicroteslas

Earth’s magnetic field strength is typically 25-65 microteslas. Both platforms return calibrated geomagnetic field values with the device’s own magnetic bias (hard iron distortion) removed.

import SwiftUI
import SkipDevice
struct MagnetometerView: View {
@State var event: MagnetometerEvent?
var heading: Double {
guard let event = event else { return 0 }
let angle = atan2(event.y, event.x) * 180.0 / .pi
return angle < 0 ? angle + 360 : angle
}
var body: some View {
VStack {
if let event = event {
Text("X: \(event.x) uT")
Text("Y: \(event.y) uT")
Text("Z: \(event.z) uT")
Text("Heading: \(heading)")
}
}
.task {
let provider = MagnetometerProvider()
guard provider.isAvailable else { return }
provider.updateInterval = 0.1
do {
for try await event in provider.monitor() {
self.event = event
}
} catch {
logger.error("magnetometer error: \(error)")
}
}
}
}
PropertyTypeDescription
xDoubleX-axis magnetic field in microteslas
yDoubleY-axis magnetic field in microteslas
zDoubleZ-axis magnetic field in microteslas
timestampTimeIntervalEvent timestamp (seconds since boot)

Measures atmospheric pressure in kilopascals (kPa) and tracks relative altitude changes in meters since monitoring began.

iOSAndroid
APICMAltimeterSensor.TYPE_PRESSURE
Pressure unitskPahPa (converted to kPa by SkipDevice)
AltitudeRelative meters since startComputed via SensorManager.getAltitude

Standard atmospheric pressure at sea level is approximately 101.325 kPa.

import SwiftUI
import SkipDevice
struct BarometerView: View {
@State var event: BarometerEvent?
var body: some View {
VStack {
if let event = event {
Text("Pressure: \(event.pressure) kPa")
Text("Relative altitude: \(event.relativeAltitude) m")
}
}
.task {
let provider = BarometerProvider()
guard provider.isAvailable else { return }
provider.updateInterval = 0.5
do {
for try await event in provider.monitor() {
self.event = event
}
} catch {
logger.error("barometer error: \(error)")
}
}
}
}
PropertyTypeDescription
pressureDoubleAtmospheric pressure in kilopascals (kPa)
relativeAltitudeDoubleAltitude change in meters since monitoring started
timestampTimeIntervalEvent timestamp
PlatformRequirement
iOSNSMotionUsageDescription (same as other motion sensors)
AndroidDeclare sensor feature in AndroidManifest.xml

Android manifest entry:

<uses-feature android:name="android.hardware.sensor.barometer" android:required="false" />

Set android:required="false" so the app can still be installed on devices without a barometer.

SensoriOS DeclarationiOS RuntimeAndroid DeclarationAndroid Runtime
Network ReachabilityNoneNoneACCESS_NETWORK_STATENone
LocationNSLocationWhenInUseUsageDescriptionYes (via PermissionManager)ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATIONYes (via PermissionManager)
AccelerometerNSMotionUsageDescriptionNoneNoneNone
GyroscopeNSMotionUsageDescriptionNoneNoneNone
MagnetometerNSMotionUsageDescriptionNoneNoneNone
BarometerNSMotionUsageDescriptionNoneuses-feature (barometer)None
ProviderEvent TypeKey PropertiesisAvailableupdateInterval
NetworkReachability.isNetworkReachable: Bool (static)
LocationProviderLocationEventlatitude, longitude, altitude, speed, course, accuracyYesNo (1s default)
AccelerometerProviderAccelerometerEventx, y, z (G’s)YesYes
GyroscopeProviderGyroscopeEventx, y, z (rad/s)YesYes
MagnetometerProviderMagnetometerEventx, y, z (microteslas)YesYes
BarometerProviderBarometerEventpressure (kPa), relativeAltitude (m)YesYes

All sensor providers share the same interface:

Method / PropertyDescription
init()Create a provider instance
isAvailable: BoolWhether the sensor hardware is present
updateInterval: TimeInterval?Set before calling monitor()
monitor() -> AsyncThrowingStreamStart streaming sensor events
stop()Stop monitoring (also called automatically on deinit and task cancellation)

This project is a Swift Package Manager module that uses the Skip plugin to build the package for both iOS and Android.

The module can be tested using the standard swift test command or by running the test target for the macOS destination in Xcode, which will run the Swift tests as well as the transpiled Kotlin JUnit tests in the Robolectric Android simulation environment.

Parity testing can be performed with skip test, which will output a table of the test results for both platforms.

We welcome contributions to this package in the form of enhancements and bug fixes.

The general flow for contributing to this and any other Skip package is:

  1. Fork this repository and enable actions from the “Actions” tab
  2. Check out your fork locally
  3. When developing alongside a Skip app, add the package to a shared workspace to see your changes incorporated in the app
  4. Push your changes to your fork and ensure the CI checks all pass in the Actions tab
  5. Add your name to the Skip Contributor Agreement
  6. Open a Pull Request from your fork with a description of your changes