# nativescript-preferences > Native app settings for NativeScript 9 (iOS + Android). Describe settings once in `app/app.preferences.ts` with `definePreferences({ items })`; keys and value types are inferred from that literal, and the same file is the runtime instance. The build hook generates the iOS `Settings.bundle` and the Android `PreferenceScreen` XML from it. One `Preferences` class reads and writes `NSUserDefaults` / `SharedPreferences`, raises change events, works as a two-way `bindingContext`, and opens the OS settings UI. Requires TypeScript 6 (the NativeScript 9 version; 5.3 and 7.0 verified). ## Setup ```bash ns plugin add nativescript-preferences npx ns-preferences init # creates app/app.preferences.ts, adds the build hook, generates once ``` `init` adds `hooks: [{ type: 'before-prepare', script: 'node_modules/nativescript-preferences/hooks/before-prepare.cjs' }]` to `nativescript.config.ts`. Every `ns run|build|prepare` then regenerates. Run `npx ns-preferences generate` by hand and `npx ns-preferences check` to fail CI when output is stale. Generated files carry a "Generated by nativescript-preferences" header; a file without it is never overwritten (`--force` overrides). Recommended: gitignore the generated files (`Settings.bundle/`, `res/xml/preferences.xml`, `res/values/preferences_arrays.xml`) and let the hook produce them. The definition is found in the app folder (`appPath` from `nativescript.config.ts`, else `src`, else `app`) or the project root; `--config ` overrides. ## app.preferences.ts ```ts import { definePreferences } from 'nativescript-preferences'; export default definePreferences({ title: 'Settings', items: [ { type: 'group', title: 'General', summary: 'Shown as footer on iOS', items: [ { key: 'enabled', type: 'toggle', title: 'Enabled', default: true }, { key: 'name', type: 'text', title: 'Name', default: '', keyboard: 'email' }, { key: 'theme', type: 'list', title: 'Theme', default: 'system', options: [{ value: 'system', title: 'Follow system' }, 'light', 'dark'] }, { key: 'topics', type: 'multilist', title: 'Topics', default: ['news'], options: ['news', 'offers'], ios: false }, { key: 'volume', type: 'slider', title: 'Volume', default: 50, min: 0, max: 100, step: 5 }, { key: 'version', type: 'label', title: 'Version', value: '1.0' }, ] }, { type: 'screen', key: 'advanced', title: 'Advanced', items: [ /* ... */ ] }, ], }); ``` ```ts import settings from './app.preferences'; settings.get('theme'); // 'system' | 'light' | 'dark' settings.get('volume'); // number settings.definition; // the object above ``` Rules: - `items` is required. Item types: `group` (no key, no nesting of groups), `screen` (nested page, needs `title`), `text` (string), `toggle` (boolean), `list` (one of `options`), `multilist` (string[]), `slider` (integer; `min`, `max`, `step`, `default` must be integers), `label` (read-only, `value`; not part of the schema). - Every non-group item needs a unique `key` matching `^[A-Za-z_][A-Za-z0-9_]*$`. Avoid keys that collide with class members (`set`, `get`, `keys`, `has`, `clear`, `definition`); they still work via `get()` but are not bindable. - Inferred types: `text` string, `toggle` boolean, `slider` number, `multilist` string[], `list` the union of its option values (`options` entries are strings or `{ value, title }`). No `as const` needed. - Compile errors: a `list` without a `default`, a `list` default not in its options, a `multilist` default outside its options, a group inside a group, a misspelled property (`titel`). The build validates the same things again and names the item. - Omitted defaults: `text` `''`, `toggle` `false`, `multilist` `[]`, `slider` `min`. Sliders round on write. - `output`: `ios` (Settings.bundle dir or `false`), `android` (res dir or `false`), `androidResource` (default `preferences`). `output.typescript` / `interfaceName` / `exportName` and `suiteName` are for `preferences.json` only and are rejected here. - Per-platform overrides on any item: `ios: false` / `android: false` hides it there. `ios: { widget: 'PSRadioGroupSpecifier' }` or `android: { widget: 'DropDownPreference' }` swaps the control; any other entry is written verbatim as a plist key or XML attribute (`'android:icon': '@drawable/ic_x'`, `'app:showSeekBarValue': true`), `null` removes one the generator would write. `widget` itself is not validated; pick a control that stores the same shape of data. Icons referenced this way are ordinary drawables you add to `App_Resources` yourself. - iOS has no multi-select control: a `multilist` is left out of Settings.bundle (a note is printed by `generate`). Set `ios: false` on it to make that explicit, or give it an `ios.widget`. - The generator lays out iOS itself: a `PSRadioGroupSpecifier` goes last in its group, and a `screen` sharing a group with rows gets its own card. Do not work around these by hand. - The file runs under Node at build time (transpiled like `nativescript.config.ts`). Keep it self-contained: import only `nativescript-preferences` (or `@nativescript/preferences`) and relative `.ts` / `.js` helpers. Any other import, `import.meta`, or top-level `await` is rejected with a message. It must `export default definePreferences(...)`. Do not put environment- or platform-dependent logic in it: the build and the app must see the same definition. ## API (`Preferences`) ```ts import { Preferences } from 'nativescript-preferences'; settings.get('volume'); // typed, never undefined for a typed schema settings.get('volume', 10); // explicit fallback settings.set('theme', 'dark'); // wrong type is a compile error settings.set('theme', null); // removes the stored value; default applies again settings.remove('theme'); settings.clear(); settings.has('theme'); settings.keys(); settings.getAll(); const stop = settings.onChange('theme', (value, data) => {}); // per key const stopAll = settings.onChange((data) => { data.key; data.value; data.oldValue; }); settings.refresh(); // re-read the native store, raise events for differences await settings.openSettings(); // iOS: the Settings app. Android: pushes a page rendering preferences.xml await settings.openSettings({ title: 'Settings', rootKey: 'advanced', modal: true }); // Android-only options settings.registerDefaults(); // iOS: Settings.bundle DefaultValues. Android: preferences.xml defaults for keys with no value, once per install (readAgain=true repeats) settings.ios / settings.android; // NSUserDefaults / SharedPreferences settings.dispose(); // stop observing (short-lived instances only) Preferences.shared; // untyped singleton; get() may be undefined; getString/getNumber/getBoolean/getStringArray coerce Preferences.applicationSettings; // the store @nativescript/core ApplicationSettings uses (same as shared on iOS; a separate prefs.db file on Android) new Preferences({ defaults, suiteName: 'group.com.example.app' }); // separate store: iOS App Group / Android SharedPreferences file new Preferences({ defaults, integers: ['volume'] }); // numbers written to these keys are rounded (do this for sliders) ``` Value types: `string | number | boolean | string[]` (a `string[]` is a set: Android stores it unordered and returns it sorted). Change events fire exactly once per write, from code, from bindings, and from the OS settings UI (iOS delivers external changes on resume). iOS system defaults (`NSHyphenatesAsLastResort`, `AppleLanguages`, `MultiWindowEnabled`...) never appear in `keys()`, `getAll()` or change events. Defaults, strongest first: the stored value, then on iOS the `Settings.bundle` default (registered after the in-code one, so it wins where both define a key), then the in-code default. ## Binding your own UI The instance is an `Observable` that mirrors every key as a property, so: ```ts page.bindingContext = settings; ``` ```xml