OneScript
Foundation

geolocation

Get the device location and compass heading, look up addresses, and let the user pick a place on a map.

geolocation reads the device location through Core Location. One-time requests return promises; continuous updates are async iterables that you stop with break or an AbortSignal. The standard Web API navigator.geolocation is also available for libraries that use it.

import { geolocation } from "onescript";

const position = await geolocation.getPosition();
console.log(position.coords.latitude, position.coords.longitude);

Types

interface Coordinate {
  latitude: number;
  longitude: number;
}

interface Position {
  coords: {
    latitude: number;
    longitude: number;
    accuracy: number;               // meters
    altitude: number | null;        // meters
    altitudeAccuracy: number | null;
    heading: number | null;         // direction of travel, degrees from true north
    speed: number | null;           // meters per second
  };
  timestamp: number;                // Unix milliseconds
}

interface Heading {
  magneticHeading: number;          // degrees from magnetic north
  trueHeading: number | null;       // degrees from true north
  accuracy: number | null;          // maximum error in degrees
  timestamp: number;
}

interface Placemark extends Coordinate {
  name?: string;
  fullAddress?: string;             // formatted for the region, may span several lines
  city?: string;
  region?: string;                  // country or region name
  regionCode?: string;              // ISO 3166-1 code, for example "CN"
  timeZone?: string;                // IANA time zone, for example "Asia/Shanghai"
}

interface GeolocationPermission {
  status: "notDetermined" | "denied" | "restricted" | "authorized";
  accuracy: "full" | "reduced";
  widgetUpdates: boolean;
}

type LocationAccuracy = "navigation" | "best" | number;

Position has the same shape as the Web GeolocationPosition. Values the system can't determine are null. Placemark fields that aren't available for a place are omitted.

LocationAccuracy sets the desired accuracy. "best" is the default and the best accuracy the device offers. "navigation" is for turn-by-turn use and uses more power. A number is the desired accuracy in meters.

Permission

function geolocation.getPermission(): Promise<GeolocationPermission>;
function geolocation.requestPermission(options?: { fullAccuracy?: boolean }): Promise<GeolocationPermission>;

getPosition and positions ask for When In Use permission the first time they're called, so you usually don't have to request it yourself. Call requestPermission to ask before you need the location, or with fullAccuracy: true to ask for precise location once when the user has turned off Precise Location.

statusMeaning
notDeterminedThe user hasn't been asked yet.
deniedThe user denied access, or Location Services are off.
restrictedAccess is restricted, for example by Screen Time.
authorizedScripts can use the location while OneScript is in use.

accuracy is "reduced" when the user turned off Precise Location; positions are then approximate. widgetUpdates reports whether widgets can get the location.

The permission prompt can only appear in the OneScript app. In the action extension and widgets, an undetermined permission throws LOCATION_CONTEXT_UNAVAILABLE; run a location script in the app once to grant it.

geolocation.getPosition

function geolocation.getPosition(options?: {
  accuracy?: LocationAccuracy;
  maximumAge?: number;
  signal?: AbortSignal;
}): Promise<Position>;

Gets the current location once.

OptionDescription
accuracyDesired accuracy. Defaults to "best".
maximumAgeAccept a cached location up to this many milliseconds old. Defaults to 0, which always gets a fresh location.
signalCancels the request. Use AbortSignal.timeout(ms) for a timeout.
const position = await geolocation.getPosition({
  maximumAge: 5 * 60_000,
  signal: AbortSignal.timeout(10_000)
});

When the signal aborts, the promise rejects with signal.reason. If the permission prompt is showing, cancellation takes effect after the user answers it.

In widgets, prefer maximumAge: a fresh fix can take several seconds, and the widget has limited time to finish.

geolocation.positions

function geolocation.positions(options?: {
  accuracy?: LocationAccuracy;
  distanceFilter?: number;
  activityType?: "other" | "automotiveNavigation" | "fitness" | "otherNavigation" | "airborne";
  signal?: AbortSignal;
}): AsyncIterable<Position>;

Receives location updates until you break out of the loop or the signal aborts. The script keeps running while the loop waits for updates.

OptionDescription
accuracyDesired accuracy. Defaults to "best".
distanceFilterMinimum distance in meters the device must move before a new update. Defaults to every movement.
activityTypeHelps iOS save power and improve accuracy. Defaults to "other".
signalStops the updates. The loop then throws signal.reason.
const track = [];

for await (const position of geolocation.positions({ distanceFilter: 10, activityType: "fitness" })) {
  track.push(position.coords);
  if (track.length === 100) break;
}

Every update is delivered, even when your loop body is slower than the updates arrive. If location access is revoked while the loop runs, the loop throws LOCATION_PERMISSION_DENIED.

Location updates only run while OneScript is in the foreground.

Heading

function geolocation.getHeading(options?: { signal?: AbortSignal }): Promise<Heading>;
function geolocation.headings(options?: { filter?: number; signal?: AbortSignal }): AsyncIterable<Heading>;

getHeading returns the current compass heading. headings delivers heading changes; filter is the minimum change in degrees, and the default is every change. Headings are a current state, so if your loop body is slow, intermediate headings are skipped and you receive the latest one.

The compass doesn't need location permission. Without it, trueHeading is null. Devices without a magnetometer, including the simulator, throw LOCATION_UNAVAILABLE.

for await (const heading of geolocation.headings({ filter: 5 })) {
  console.log(Math.round(heading.trueHeading ?? heading.magneticHeading));
}

Geocoding

function geolocation.geocode(address: string, options?: { locale?: string }): Promise<Placemark[]>;
function geolocation.reverseGeocode(coordinate: Coordinate, options?: { locale?: string }): Promise<Placemark[]>;

geocode converts an address to placemarks; reverseGeocode converts a coordinate to placemarks. Both resolve with an empty array when nothing matches. locale is the language of the results as a BCP 47 tag and defaults to the device's preferred language.

const [place] = await geolocation.reverseGeocode(position.coords, { locale: "en-US" });
console.log(place?.city, place?.region);

const results = await geolocation.geocode("1 Infinite Loop, Cupertino");

Geocoding is a network service provided by Apple and is rate limited. When it fails, or when there are too many requests, the promise rejects with LOCATION_GEOCODE_FAILED.

geolocation.pick

function geolocation.pick(options?: { initial?: Coordinate }): Promise<Coordinate | null>;

Shows a map where the user drags the map under a pin or searches for a place, then taps Done. Resolves with the chosen coordinate, or null when the user cancels. initial sets where the map starts; by default it starts at the current location.

const place = await geolocation.pick();
if (place) {
  const [placemark] = await geolocation.reverseGeocode(place);
  console.log(placemark?.name);
}

pick is only available in scripts run by the OneScript app.

Availability

APIAppAction extensionWidget
getPermission✓✓✓
requestPermission✓Only when already determinedOnly when already determined
getPosition✓✓ when authorized✓ when authorized
positions, getHeading, headings✓✓Not useful: the widget ends after widget.show
geocode, reverseGeocode✓✓✓
pick✓——

Errors

Errors have a code property:

codeMeaning
LOCATION_PERMISSION_DENIEDLocation access is denied or restricted, or the widget isn't allowed to use the location.
LOCATION_UNAVAILABLEThe location or heading couldn't be determined, or the device has no compass.
LOCATION_CONTEXT_UNAVAILABLEThe operation isn't available here, for example a permission prompt outside the app.
LOCATION_GEOCODE_FAILEDGeocoding failed.

Invalid arguments throw TypeError.

The Web Geolocation API is available as navigator.geolocation, so npm packages that use the browser location work unchanged. getCurrentPosition, watchPosition, and clearWatch support the standard enableHighAccuracy, timeout, and maximumAge options; errors are GeolocationPositionError with codes PERMISSION_DENIED (1), POSITION_UNAVAILABLE (2), and TIMEOUT (3).

const id = navigator.geolocation.watchPosition(
  position => console.log(position.coords.latitude),
  error => console.error(error.code, error.message),
  { enableHighAccuracy: true, timeout: 10_000 }
);

// The script keeps running until the watch is cleared.
setTimeout(() => navigator.geolocation.clearWatch(id), 60_000);

navigator.permissions.query({ name: "geolocation" }) returns "granted", "denied", or "prompt". The returned status doesn't fire change events.

How is this guide?

Last updated on

On this page