GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Skip
  • Documentation
  • Blog
  • Sponsor
  • Forums
  • Slack
  • Mastodon
  • Twitter
  • GitHub
  • RSS
  • Getting Started
  • Project Structure
  • Lite and Fuse Modes
  • App Development

This view

  • Original page
  • New address

GeezerVison is showing skip.dev — Sensors

Sensors

Source Repository Releases for skip-device

This framework is available at github.com/skiptools/skip-device ↗ and can be checked out and improved locally as described in the Contribution Guide.

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

On Apple platforms, the module wraps platform APIs such as UIKit ↗, CoreMotion ↗, CoreLocation ↗, and SystemConfiguration ↗. On Android, it wraps platform APIs such as Build ↗, Application ↗, Service ↗, SensorManager ↗, LocationManager ↗, and ConnectivityManager ↗.

All sensor providers expose a unified AsyncThrowingStream interface that works identically on both platforms.

Setup

Section titled “Setup”

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")
])
]
)

Sensor Usage Pattern

Section titled “Sensor Usage Pattern”

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.

Network Reachability

Section titled “Network Reachability”

Check whether the device currently has network access.

iOSAndroid
APISCNetworkReachability ↗ConnectivityManager ↗
import SkipDevice
let isReachable = NetworkReachability.isNetworkReachable

Network Reachability Permissions

Section titled “Network Reachability Permissions”
PlatformRequirement
iOSNo permission required
AndroidDeclare ACCESS_NETWORK_STATE in AndroidManifest.xml

Android manifest entry:

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

Device Identity

Section titled “Device Identity”

Read low-level device identity fields from the current platform.

iOS / tvOSAndroid
APIUIDeviceBuild, Settings.Global, Settings.Secure
import SkipDevice
let identity = DeviceIdentity.current
print(identity.name ?? "Unnamed device")
print(identity.model ?? "Unknown model")

vendorIdentifier maps to UIDevice.identifierForVendor on Apple platforms and Settings.Secure.ANDROID_ID on Android. Treat it as privacy-sensitive app data; app-specific policy, disclosure, and storage choices remain app-owned.

DeviceIdentity Properties

Section titled “DeviceIdentity Properties”
PropertyDescription
nameUser-visible device name when the platform exposes one
modelPlatform model string
localizedModelLocalized Apple model string when available
systemNamePlatform operating system name
systemVersionPlatform operating system version
vendorIdentifierApp/vendor-scoped stable identifier when available
manufacturerDevice manufacturer, such as Apple, Google, or Samsung
brandAndroid Build.BRAND when available
deviceAndroid Build.DEVICE when available
productAndroid Build.PRODUCT when available

Location

Section titled “Location”

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

iOSAndroid
APICLLocationManager ↗LocationManager ↗ (FUSED_PROVIDER)

Single Location Request

Section titled “Single Location Request”
import SkipDevice
let provider = LocationProvider()
let location = try await provider.fetchCurrentLocation()
print("lat: \(location.latitude), lon: \(location.longitude), alt: \(location.altitude)")

Continuous Location Updates

Section titled “Continuous Location Updates”
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)"
}
}
}
}

LocationEvent Properties

Section titled “LocationEvent Properties”
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 Permissions

Section titled “Location Permissions”

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"/>

Application Runtime Events

Section titled “Application Runtime Events”

Monitor app lifecycle and memory pressure events through a single API on both platforms.

iOS / tvOSAndroid
Lifecycle APIUIApplication notificationsApplication.ActivityLifecycleCallbacks
Memory APIUIApplication.didReceiveMemoryWarningNotificationComponentCallbacks2
import SkipDevice
let provider = ApplicationRuntimeProvider()
Task {
for await event in provider.monitorLifecycle() {
print("event: \(event.kind.rawValue), phase: \(event.phase.rawValue)")
}
}
Task {
for await event in provider.monitorMemoryPressure() {
print("memory pressure: \(event.level.rawValue)")
}
}

event.kind preserves an iOS-style lifecycle event name where possible, while event.phase gives callers a normalized foreground/background phase. Call provider.stop() when the owning feature no longer needs runtime events. Unsupported Apple platforms compile and report .unknown lifecycle phase with no platform callbacks.

Android memory pressure maps onLowMemory, TRIM_MEMORY_RUNNING_CRITICAL, and TRIM_MEMORY_COMPLETE to .critical; other trim-memory pressure callbacks map to .warning.

Runtime Event Values

Section titled “Runtime Event Values”
TypeValues
ApplicationLifecyclePhaseactive, inactive, background, terminated, unknown
ApplicationLifecycleEventKinddidBecomeActive, willResignActive, didEnterBackground, willTerminate, unknown
MemoryPressureLevelwarning, critical

monitorLifecycle() immediately yields the most recently known lifecycle event. On Android this is initially .unknown until an activity lifecycle callback is observed. On Apple platforms, the initial phase is read from UIApplication.shared.applicationState when available on the main thread; otherwise it starts as .unknown.

Background Activity

Section titled “Background Activity”

Begin and end finite user-visible background work. This is not a guarantee of indefinite execution: the app still owns completing work promptly and ending the activity.

import SkipDevice
let identifier = try await BackgroundActivity.begin(BackgroundActivityRequest(
name: "Syncing media",
reason: BackgroundActivityReason.localNetworkTransfer,
detail: "Keeping the transfer active"
))
await performTransfer()
await BackgroundActivity.end(identifier)

Use do / catch or task cancellation handling in app code so BackgroundActivity.end(_:) runs on success, failure, and cancellation.

BackgroundActivityRequest Properties

Section titled “BackgroundActivityRequest Properties”
PropertyDefaultDescription
nameRequiredUser-visible activity name
reasonshortCriticalWorkPlatform reason used to choose the Android foreground-service type
detailEmpty stringOptional user-visible detail for the Android foreground notification
notificationChannelIDtools.skip.device.background_activityAndroid notification channel identifier
notificationID41001Android foreground notification identifier
notificationIconResourceNameic_notificationAndroid drawable resource name for the foreground notification icon

Background Activity Reasons

Section titled “Background Activity Reasons”
ReasonAndroid foreground service type
localNetworkTransferdataSync
mediaProcessingmediaProcessing on Android 15+, dataSync on older Android versions
connectedDeviceTransferconnectedDevice
shortCriticalWorkshortService when available, dataSync on older Android versions

On iOS and tvOS, BackgroundActivity wraps UIApplication.beginBackgroundTask(withName:expirationHandler:) and UIApplication.endBackgroundTask(_:).

On Android, BackgroundActivity starts skip.device.BackgroundActivityService as a foreground service. Android 15 limits dataSync and mediaProcessing foreground services to 6 hours per 24 hours; the service implements Service.onTimeout(int, int) and stops promptly when Android reports a timeout.

Background Activity Android Manifest

Section titled “Background Activity Android Manifest”

Apps using BackgroundActivity must declare the foreground service and the service-type permissions needed by their chosen reasons:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING" />
<application>
<service
android:name="skip.device.BackgroundActivityService"
android:exported="false"
android:foregroundServiceType="dataSync|mediaProcessing|connectedDevice|shortService" />
</application>

shortService does not have a type-specific permission, but it still requires FOREGROUND_SERVICE. connectedDevice has additional Android runtime prerequisites depending on the device transport, such as Bluetooth, NFC, USB, or network-change capabilities. The app owns any extra runtime permissions required for its use case.

The default Android notification icon resource is ic_notification. Apps can provide a different drawable resource through BackgroundActivityRequest.notificationIconResourceName. Notification text, icon design, Android notification permission flow, and Google Play foreground-service policy justification remain app-owned.

Motion Sensors

Section titled “Motion Sensors”

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.

Motion Permissions

Section titled “Motion 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 …"

Accelerometer

Section titled “Accelerometer”

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.startAccelerometerUpdates ↗Sensor.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)")
}
}
}
}

AccelerometerEvent Properties

Section titled “AccelerometerEvent Properties”
PropertyTypeDescription
xDoubleX-axis acceleration in G’s
yDoubleY-axis acceleration in G’s
zDoubleZ-axis acceleration in G’s
timestampTimeIntervalEvent timestamp (seconds since boot)

Gyroscope

Section titled “Gyroscope”

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

iOSAndroid
APICMMotionManager.startGyroUpdates ↗Sensor.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)")
}
}
}
}

GyroscopeEvent Properties

Section titled “GyroscopeEvent Properties”
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)

Magnetometer

Section titled “Magnetometer”

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)")
}
}
}
}

MagnetometerEvent Properties

Section titled “MagnetometerEvent Properties”
PropertyTypeDescription
xDoubleX-axis magnetic field in microteslas
yDoubleY-axis magnetic field in microteslas
zDoubleZ-axis magnetic field in microteslas
timestampTimeIntervalEvent timestamp (seconds since boot)

Barometer

Section titled “Barometer”

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

iOSAndroid
APICMAltimeter ↗Sensor.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)")
}
}
}
}

BarometerEvent Properties

Section titled “BarometerEvent Properties”
PropertyTypeDescription
pressureDoubleAtmospheric pressure in kilopascals (kPa)
relativeAltitudeDoubleAltitude change in meters since monitoring started
timestampTimeIntervalEvent timestamp

Barometer Permissions

Section titled “Barometer Permissions”
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.

Permissions Summary

Section titled “Permissions Summary”
CapabilityiOS DeclarationiOS RuntimeAndroid DeclarationAndroid Runtime
Network ReachabilityNoneNoneACCESS_NETWORK_STATENone
Device IdentityNoneNoneNoneNone
LocationNSLocationWhenInUseUsageDescriptionYes (via PermissionManager)ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATIONYes (via PermissionManager)
Application Runtime EventsNoneNoneNoneNone
Background ActivityNoneNoneFOREGROUND_SERVICE plus selected foreground-service type permissionsApp-owned by use case
AccelerometerNSMotionUsageDescriptionNoneNoneNone
GyroscopeNSMotionUsageDescriptionNoneNoneNone
MagnetometerNSMotionUsageDescriptionNoneNoneNone
BarometerNSMotionUsageDescriptionNoneuses-feature (barometer)None

API Reference

Section titled “API Reference”
APIEvent / Value TypeKey PropertiesisAvailableupdateInterval
NetworkReachability—.isNetworkReachable: Bool (static)——
DeviceIdentityDeviceIdentity.current, name, model, system, vendor, Android build fields——
LocationProviderLocationEventlatitude, longitude, altitude, speed, course, accuracyYesNo (1s default)
ApplicationRuntimeProviderApplicationLifecycleEvent, MemoryPressureEventlifecycle phase/kind, memory pressure level——
BackgroundActivityBackgroundActivityRequestbegin(_:), end(_:), reason, notification metadata——
AccelerometerProviderAccelerometerEventx, y, z (G’s)YesYes
GyroscopeProviderGyroscopeEventx, y, z (rad/s)YesYes
MagnetometerProviderMagnetometerEventx, y, z (microteslas)YesYes
BarometerProviderBarometerEventpressure (kPa), relativeAltitude (m)YesYes

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)

Building

Section titled “Building”

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

Testing

Section titled “Testing”

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.

Contributing

Section titled “Contributing”

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

Releases

Section titled “Releases”

The 10 most recent releases of skiptools/skip-device ↗:

  • Release 0.6.1 ↗ — 2026-10-06
  • Release 0.6.0 ↗ — 2026-09-03
  • Release 0.5.3 ↗ — 2026-06-18
  • Release 0.5.2 ↗ — 2026-06-04
  • Release 0.5.1 ↗ — 2026-06-01
  • Release 0.5.0 ↗ — 2026-04-01
  • Release 0.4.2 ↗ — 2025-07-14
  • Release 0.4.1 ↗ — 2025-06-28
  • Release 0.4.0 ↗ — 2025-04-05
  • Release 0.3.2 ↗ — 2025-02-24

Full history: github.com/skiptools/skip-device/releases ↗ — .atom feed ↗

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.