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.
status | Meaning |
|---|---|
notDetermined | The user hasn't been asked yet. |
denied | The user denied access, or Location Services are off. |
restricted | Access is restricted, for example by Screen Time. |
authorized | Scripts 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.
| Option | Description |
|---|---|
accuracy | Desired accuracy. Defaults to "best". |
maximumAge | Accept a cached location up to this many milliseconds old. Defaults to 0, which always gets a fresh location. |
signal | Cancels 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.
| Option | Description |
|---|---|
accuracy | Desired accuracy. Defaults to "best". |
distanceFilter | Minimum distance in meters the device must move before a new update. Defaults to every movement. |
activityType | Helps iOS save power and improve accuracy. Defaults to "other". |
signal | Stops 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
| API | App | Action extension | Widget |
|---|---|---|---|
getPermission | ✓ | ✓ | ✓ |
requestPermission | ✓ | Only when already determined | Only 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:
code | Meaning |
|---|---|
LOCATION_PERMISSION_DENIED | Location access is denied or restricted, or the widget isn't allowed to use the location. |
LOCATION_UNAVAILABLE | The location or heading couldn't be determined, or the device has no compass. |
LOCATION_CONTEXT_UNAVAILABLE | The operation isn't available here, for example a permission prompt outside the app. |
LOCATION_GEOCODE_FAILED | Geocoding failed. |
Invalid arguments throw TypeError.
navigator.geolocation
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