diff --git a/content/configuration/vite.md b/content/configuration/vite.md new file mode 100644 index 00000000..433ad5cc --- /dev/null +++ b/content/configuration/vite.md @@ -0,0 +1,241 @@ +--- +title: Configuring Vite +description: NativeScript apps can be bundled with Vite. +contributors: + - NathanWalker +--- + +All NativeScript apps can be bundled using [Vite](https://vite.dev/). To manage the required configuration, we maintain the `@nativescript/vite` package. + +## Setup + +Install the plugin. + +```bash +npm install @nativescript/vite +``` + +## Quick start (`init`) + +To bootstrap an existing NativeScript app for Vite, run from your app root: + +```bash +npx nativescript-vite init +``` + +This will: + +- Generate a `vite.config.ts` using the detected project flavor (Angular, Vue, React, Solid, TypeScript, or JavaScript) and the corresponding helper from `@nativescript/vite`. +- Add (or update) the following npm scripts in your app `package.json`: + - `dev:ios` + - `dev:android` + - `dev:server:ios` + - `dev:server:android` + - `ios` + - `android` +- Add the devDependencies `concurrently` and `wait-on`. +- Add the dependency `@valor/nativescript-websockets`. +- Append `.ns-vite-build` to `.gitignore` if it is not already present. + +After running `init`, you now have two ways to work with Vite: + +1. HMR workflow + +```bash +npm run dev:ios +``` + +2. Standard dev workflow (non-HMR) + +```bash +ns debug ios --no-hmr +ns debug android --no-hmr +``` + +## Configure + +The plugin comes with several framework integrations. + +### Vue + +```ts +import { defineConfig, mergeConfig, UserConfig } from 'vite'; +import { vueConfig } from '@nativescript/vite'; + +export default defineConfig(({ mode }): UserConfig => { + return mergeConfig(vueConfig({ mode }), {}); +}); +``` + +### Angular + +```ts +import { defineConfig, mergeConfig, UserConfig } from 'vite'; +import { angularConfig } from '@nativescript/vite'; + +export default defineConfig(({ mode }): UserConfig => { + return mergeConfig(angularConfig({ mode }), {}); +}); +``` + +### Solid + +```ts +import { defineConfig, mergeConfig, UserConfig } from 'vite'; +import { solidConfig } from '@nativescript/vite'; + +export default defineConfig(({ mode }): UserConfig => { + return mergeConfig(solidConfig({ mode }), {}); +}); +``` + +### Svelte + +```ts +import { defineConfig, mergeConfig, UserConfig } from 'vite'; +import { solidConfig } from '@nativescript/vite'; + +export default defineConfig(({ mode }): UserConfig => { + return mergeConfig(solidConfig({ mode }), {}); +}); +``` + +### React + +```ts +import { defineConfig, mergeConfig, UserConfig } from 'vite'; +import { reactConfig } from '@nativescript/vite'; + +export default defineConfig(({ mode }): UserConfig => { + return mergeConfig(reactConfig({ mode }), {}); +}); +``` + +### TypeScript (XML view) + +```ts +import { defineConfig, mergeConfig, UserConfig } from 'vite'; +import { typescriptConfig } from '@nativescript/vite'; + +export default defineConfig(({ mode }): UserConfig => { + return mergeConfig(typescriptConfig({ mode }), {}); +}); +``` + +The above config configures most things required to bundle a NativeScript application. + +This page contains examples of common things you might want to change in the [Examples of configurations section](#configuration-examples) - for anything else not mentioned here, refer to the [Vite documentation](https://vite.dev/config/). + +## CLI Flags + +When running a NativeScript app the following flags have an effect on the final vite config + +### --no-hmr + +Disable HMR (enabled by default) + +### --env.verbose + +Prints verbose logs and the internal config before building + +### Additional flags + +Additional env flags that are usually passed by the CLI automatically + +- `--env.appPath` - path to the app source (same as `appPath` in the `nativescript.config.ts`) +- `--env.appResourcesPath` - path to App_Resources (same as `appResourcesPath` in the `nativescript.config.ts`) +- `--env.nativescriptLibPath` - path to the currently running CLI's library. +- `--env.android` - `true` when running on android +- `--env.ios` - `true` when running on ios +- `--env.platform=` - for specifying the platform to use. Can be `android` or `ios`, or a custom platform in the future. +- `--env.hmr` - `true` when building with HMR enabled + +## Global "magic" variables + +We define a few useful globally available variables that you can use to alter logic in your applications. + +- `__DEV__` - `true` when webpack is building in development mode + ```ts + if (__DEV__) { + // we are running a dev build + } + ``` +- `__ANDROID__`, `true` when the platform is Android + ```ts + if (global.isAndroid) { + // we are running on android + } + ``` +- `__IOS__`, `true` when the platform is iOS + ```ts + if (__IOS__) { + // we are running on iOS + } + ``` + +::: details The following variables are also defined, but are primarily intended to be used by NativeScript Core internally, or plugins that wish to use these. + +- `__NS_ENV_VERBOSE__` - `true` when `--env.verbose` is set +- `__CSS_PARSER__` - the CSS parser used by NativeScript Core. The value is set based on the `cssParser` value in the `nativescript.config.ts` and defaults to `css-tree` +- `__UI_USE_XML_PARSER__` - a flag used by NativeScript Core to disable the XML parser when it's not used +- `__UI_USE_EXTERNAL_RENDERER__` - a flag used by NativeScript Core to disable registering global modules when an external renderer is used. + +::: + +## Configuration examples + +Here are some common examples of things you may want to do in your `vite.config.ts`. + +### Adding a copy rule + +```ts +import { defineConfig, mergeConfig, UserConfig } from 'vite'; +import { typescriptConfig } from '@nativescript/vite'; +import path from 'path'; +import { viteStaticCopy } from 'vite-plugin-static-copy'; + +export default defineConfig(({ mode }): UserConfig => { + const base = typescriptConfig({ mode }); + const projectRoot = path.resolve(__dirname); + const testImagePath = path.resolve(projectRoot, 'src/ui/image/700x50.png'); + return mergeConfig(base, { + plugins: [ + viteStaticCopy({ + targets: [{ src: testImagePath, dest: 'ui/image' }], + }) + ], + }); +}); + +``` + +## Plugin API + +NativeScript plugins can provide a `nativescript.vite.mjs` file in their root folder (next to `package.json`), and `@nativescript/vite` will include these configs when resolving the final config. + +For example, a plugin could return custom processing: + +```js +import { createRequire } from "module"; +const require = createRequire(import.meta.url); + +let postcssConfig = "./postcss.config.js"; + +try { + const twV4 = require("@tailwindcss/postcss"); + const nsTailwind = require("@nativescript/tailwind"); + postcssConfig = { plugins: [twV4, nsTailwind] }; +} catch (e2) { + console.warn( + "Inline PostCSS unavailable, falling back to ./postcss.config.js" + ); +} + +export default () => { + return { + css: { + postcss: postcssConfig, + }, + }; +}; +``` diff --git a/content/core/utils.md b/content/core/utils.md index 8b1fa2ad..02bfc796 100644 --- a/content/core/utils.md +++ b/content/core/utils.md @@ -87,7 +87,6 @@ const labelHierarchy: Array = Utils.getBaseClasses(new Label()) ] */ ``` -### Hiding a keyboard To hide a soft keyboard on the screen, use the [dismissKeyboard()](#dismisskeyboard) method. @@ -101,7 +100,6 @@ Utils.dismissKeyboard() ```ts const majorVersion: number = Utils.ios -``` (`iOS only`) Gets the iOS device major version. For example, for `16.0` ,it returns `16`. @@ -115,7 +113,6 @@ const isFileOrResourcePath: boolean = Utils.isFileOrResourcePath(path) Returns `true` if the specified path points to a resource or local file. ---- ### isDataURI() @@ -142,7 +139,6 @@ const escapedString: string = Utils.escapeRegexSymbols(string) ``` Escapes special regex characters (`.`, `*`, `^`, `$` and so on) in a string and returns a valid regex. - --- ### convertString() @@ -171,14 +167,10 @@ A utility function that invokes garbage collection on the JavaScript side. Utils.queueMacrotask(task: () => void) ``` -Queues the passed function to be ran as a macroTask. - ---- ### queueGC() ```ts -Utils.queueGC(delay, useThrottle) ``` - _Optional_: `delay` time, in milliseconds, to wait before garbage collection starts. @@ -186,7 +178,6 @@ Utils.queueGC(delay, useThrottle) A utility function that queues a garbage collection. Multiple calls in quick succession are debounced by default and only one gc will be executed after 900ms. ---- ### debounce() @@ -194,7 +185,6 @@ A utility function that queues a garbage collection. Multiple calls in quick suc const debouncedFn = Utils.debounce(fn, delay) debouncedFn() -``` A simple debounce utility. @@ -202,7 +192,6 @@ A simple debounce utility. - _Optional_:`delay` delays the bouncing, in milliseconds. Defaults to 300ms. --- - ### throttle() ```ts @@ -215,7 +204,6 @@ A simple throttle utility. - `fn` The function to throttle. - _Optional_:`delay` delays the throttling, in milliseconds. Defaults to 300ms. - --- ### isFontIconURI() @@ -223,7 +211,6 @@ A simple throttle utility. ```ts const isFontIconURI: boolean = Utils.isFontIconURI('font://') ``` - Returns true if the specified URI is a font icon URI. --- @@ -235,7 +222,6 @@ Utils.executeOnMainThread(fn: Function) ``` -Checks if the current thread is the main thread. If it is, calls the passed function. Otherwise, it dispatches it to the main thread. :::warning Important! @@ -252,7 +238,6 @@ Utils.executeOnUIThread(fn: Function) ``` Runs the passed function on the UI Thread. - :::warning Important! Always dispatches asynchronously to the UI thread. @@ -525,6 +510,8 @@ Hides any keyboard on the screen. --- +## Utils.android + ### getApplication() ```ts @@ -535,6 +522,19 @@ const app: android.app.Application = Utils.android.getApplication() --- +### getCurrentActivity() + +```ts +const activity: + | androidx.appcompat.app.AppCompatActivity + | android.app.Activity + | null = Utils.android.getCurrentActivity() +``` + +(`Android-only`) Gets the current foreground Android Activity if available, otherwise `null`. + +--- + ### getApplicationContext() ```ts @@ -545,6 +545,123 @@ Utils.android.getApplicationContext() --- +### getResources() + +```ts +const resources: android.content.res.Resources = Utils.android.getResources() +``` + +(`Android-only`) Gets the native Android [Resources](https://developer.android.com/reference/android/content/res/Resources) for the application. + +--- + +### getPackageName() + +```ts +const pkg: string = Utils.android.getPackageName() +``` + +(`Android-only`) Gets the application package name. + +--- + +### enableEdgeToEdge() + +```ts +function enableEdgeToEdge( + activity: androidx.appcompat.app.AppCompatActivity, + options?: { + statusBarLightColor?: Color + statusBarDarkColor?: Color + navigationBarLightColor?: Color + navigationBarDarkColor?: Color + handleDarkMode?: ( + bar: 'status' | 'navigation', + resources: android.content.res.Resources, + ) => boolean + }, +): void +``` + +Enables full edge-to-edge rendering on Android and lets you customize system UI overlay colors and behavior. + +- `activity` The current `AppCompatActivity` instance. +- _Optional_ `options` may include: + - `statusBarLightColor` Color to use for light appearance of the status bar (icons dark on light background). + - `statusBarDarkColor` Color to use for dark appearance of the status bar (icons light on dark background). + - `navigationBarLightColor` Color to use for light appearance of the navigation bar. + - `navigationBarDarkColor` Color to use for dark appearance of the navigation bar. + - `handleDarkMode` Decide whether light or dark system UI should be used for the given bar based on your own logic. Return `true` to use light appearance, `false` for dark. + +Notes: +- Works together with Page `androidOverflowEdge` and `androidOverflowInset` to control inset application/consumption. +- When insets are applied, they are added to the view's padding. + +Example: + +```ts +import { Application, Utils, Color } from '@nativescript/core' + +const activity = Utils.android.getCurrentActivity() + +Utils.android.enableEdgeToEdge(activity, { + statusBarLightColor: new Color('#FFFFFF'), + statusBarDarkColor: new Color('#000000'), + navigationBarLightColor: new Color('#FFFFFF'), + navigationBarDarkColor: new Color('#000000'), + handleDarkMode: (bar, resources) => { + // Decide per your theme; return true for light appearance + return true + }, +}) +``` + +--- + +### setDarkModeHandler() + +```ts +Utils.android.setDarkModeHandler(options?: { + activity?: androidx.appcompat.app.AppCompatActivity + handler: ( + bar: 'status' | 'navigation', + resources: android.content.res.Resources, + ) => boolean +}) +``` + +(`Android-only`) Sets a handler to decide whether the specified system bar should use light or dark appearance based on your logic. + +--- + +### setNavigationBarColor() + +```ts +Utils.android.setNavigationBarColor(options?: { + activity?: androidx.appcompat.app.AppCompatActivity + lightColor?: Color + darkColor?: Color +}) +``` + +(`Android-only`) Sets the navigation bar color for the application for light/dark appearances. + +--- + +### setStatusBarColor() + +```ts +Utils.android.setStatusBarColor(options?: { + activity?: androidx.appcompat.app.AppCompatActivity + lightColor?: Color + darkColor?: Color +}) +``` + +(`Android-only`) Sets the status bar color for the application for light/dark appearances. + +--- + ### getInputMethodManager() ```ts @@ -566,6 +683,16 @@ Utils.android.showSoftInput(nativeView) --- +### dismissSoftInput() + +```ts +Utils.android.dismissSoftInput(nativeView?) +``` + +(`Android-only`) Hides the soft input method (soft keyboard). Optionally provide a specific `android.view.View`. + +--- + ### stringArrayToStringSet() ```ts @@ -607,39 +734,50 @@ Gets the string id from a given resource name. --- -### getPaletteColor() +### getId() ```ts -const paletteColor: number = Utils.android.resources.getPaletteColor( - resourceName, - Utils.android.getApplicationContext(), -) +const viewId: number = Utils.android.resources.getId(resourceName) ``` -Gets a color from the current theme. +Gets an `id` resource by name. --- -### joinPaths() +### getResource() ```ts -const joinedPath: string = Utils.ios.joinPaths('photos', 'cat.png') +const resId: number = Utils.android.resources.getResource(name, type?) ``` -Joins the passed strings into a path. +Gets a resource identifier by name with an optional type. This sets an explicit package name under the hood. +See Android's [`Resources.getIdentifier`](https://developer.android.com/reference/android/content/res/Resources#getIdentifier(java.lang.String,%20java.lang.String,%20java.lang.String)). --- +### getPaletteColor() + +```ts +const paletteColor: number = Utils.android.resources.getPaletteColor( + resourceName, + Utils.android.getApplicationContext(), +) +``` + +Gets a color from the current theme. + ### getWindow() ```ts -const window: UIWindow = Utils.ios.getWindow() +const window: android.view.Window = Utils.android.getWindow() ``` -Gets the UIWindow of the app. +*Deprecated*. Use the generic `Utils.getWindow()` instead. --- +## Utils.ios + ### copyToClipboard() ```ts @@ -708,6 +846,94 @@ Create a [UIDocumentInteractionControllerDelegate](https://developer.apple.com/d --- +### getMainScreen() + +```ts +const screen: UIScreen = Utils.ios.getMainScreen() +``` + +(`iOS only`) Returns the main UIScreen. + +--- + +### isLandscape() (deprecated) + +```ts +const landscape: boolean = Utils.ios.isLandscape() +``` + +(`iOS only`) Deprecated. Use `Application.orientation` instead. + +--- + +### snapshotView() + +```ts +const image: UIImage = Utils.ios.snapshotView(view: UIView, scale: number) +``` + +(`iOS only`) Takes a snapshot image of the provided `UIView` at the desired screen `scale`. + +--- + +### applyRotateTransform() + +```ts +const result: CATransform3D = Utils.ios.applyRotateTransform( + transform: CATransform3D, + x: number, + y: number, + z: number, +) +``` + +(`iOS only`) Returns a transform rotated by the specified degrees around the X, Y and Z axes. + +--- + +### printCGRect() + +```ts +Utils.ios.printCGRect(rect: CGRect) +``` + +(`iOS only`) Debug utility to print `CGRect` values in logs (printing `CGRect` directly may appear blank otherwise). + +--- + +### copyLayerProperties() + +```ts +Utils.ios.copyLayerProperties( + view: UIView, + toView: UIView, + customProperties?: { view?: Array; layer?: Array }, +) +``` + +(`iOS only`) Copies layer-related properties from one view to another. You can provide custom property lists for `view` and `layer`. + +--- + +### animateWithSpring() + +```ts +Utils.ios.animateWithSpring(options?: { + tension?: number + friction?: number + mass?: number + delay?: number + velocity?: number + animateOptions?: UIViewAnimationOptions + animations?: () => void + completion?: (finished?: boolean) => void +}) +``` + +(`iOS only`) Animates property changes with a configurable spring effect. + +--- + ### jsArrayToNSArray() ```ts @@ -728,6 +954,18 @@ const nsArrayToJSArray: Array = nsArrayToJSArray(a: NSArray) --- +### getWindow() + +```ts +const window: UIWindow = Utils.ios.getWindow() +``` + +Gets the UIWindow of the app. + +*Deprecated*. Use the generic `Utils.getWindow()` instead. + +--- + ## API Reference(s) - [@nativescript/core/utils](https://docs.nativescript.org/api/namespace/Utils) module diff --git a/content/guide/hooks.md b/content/guide/hooks.md new file mode 100644 index 00000000..bb0589aa --- /dev/null +++ b/content/guide/hooks.md @@ -0,0 +1,504 @@ +--- +title: Hooks +description: A Guide to using CLI hooks. +contributors: + - jcassidyav +--- + +## Overview + +NativeScript hooks are executable pieces of code or Node.js scripts that can be added by application or plugin developers to customize the execution of particular NativeScript commands. They provide the power to perform special activities by plugging into different parts of the build process of your application. + +Hooks are added to the `hooks/` folder of a project by plugins, or are specified in the `nativescript.config.ts` by an application. + +For example, when `ns prepare ...` is executed, all script files in the `hooks/before-prepare/` and `hooks/after-prepare/` folders are executed as well. + +Starting with NativeScript 9.0 CLI (`npm install -g nativescript`), you can create hooks as either CommonJS with extension `.js` or ECMAScript Modules (ESM) with extension `.mjs`. + +For versions of the NativeScript CLI < 9.0 hooks must be written in CommonJS with extension `.js`. + +## Hook Execution Types + +The NativeScript CLI supports two different ways of executing hooks: + +**1. In-Process Execution** + +- Available only for JavaScript hooks +- Executes within the CLI process +- Provides access to NativeScript CLI services via dependency injection +- Determined by the presence of `module.exports` statement +- **Recommended approach** for writing hooks + +**2. Spawned Execution** + +- Executed via Node.js's `child_process.spawn` function +- Run from the project's root directory +- Cannot access NativeScript CLI services +- If a hook returns a non-zero exit code, the NativeScript command will throw an exception + +## In-Process Hooks + +**Basic Module Definition** + +To write an in-process hook, use the following module definition: +::: code-group + +```javascript [thehook.mjs (ESM)] +export default function (hookArgs) { + // Hook implementation ESM +} +``` + +```javascript [thehook.js (CommonJS)] +module.exports = function () { + // Hook implementation CommonJs +} +``` + +::: +**Using hookArgs** + +The hook function can accept a special argument named `hookArgs`, which is an object containing all arguments passed to the hooked method. + +**Example:** + +If the NativeScript CLI has a method `prepareJSApp` defined as: + +```typescript +@hook("prepareJSApp") +public async prepareJSApp(projectData: IProjectData, platformData: IPlatformData) { } +``` + +Then `hookArgs` will have the following structure: + +```json +{ + "projectData": {...}, + "platformData": {...} +} +``` + +**Using hookArgs in your hook:** +::: code-group + +```javascript [thehook.mjs (ESM)] +export default function (hookArgs) { + console.log(JSON.stringify(hookArgs.prepareData)) +} +``` + +```javascript [thehook.js (CommonJS)] +module.exports = function (hookArgs) { + console.log(hookArgs.projectData) +} +``` + +::: +**Dependency Injection** + +NativeScript CLI is built with Dependency Injection and executes in-process hooks in a way that allows you to use any registered service from the injector. + +**_Approach 1: Direct Service Injection_** + +::: code-group + +```javascript [thehook.mjs (ESM)] +export default function ($logger, $fs, $projectDataService, hookArgs) { + $logger.info('Executing hook') + // Use $fs, $projectDataService, etc. +} +``` + +```javascript [thehook.js (CommonJS)] +module.exports = function ($logger, $fs, $projectDataService, hookArgs) { + $logger.info('Executing hook') + // Use $fs, $projectDataService, etc. +} +``` + +::: +**_Approach 2: Injector Resolution_** + +::: code-group + +```javascript [thehook.mjs (ESM)] +export default function ($injector, hookArgs) { + const $logger = $injector.resolve('$logger') + const $fs = $injector.resolve('$fs') + + $logger.info('Executing hook') +} +``` + +```javascript [thehook.js (CommonJS)] +module.exports = function ($injector, hookArgs) { + const $logger = $injector.resolve('$logger') + const $fs = $injector.resolve('$fs') + + $logger.info('Executing hook') +} +``` + +::: + +**Important Notes:** + +- Injected dependencies are resolved by name +- If you inject a non-existent service (e.g., `$logger1`), the CLI won't execute the hook and will show a warning +- When using `$injector` directly, no warning is shown for incorrect service names, and an error will be thrown during execution + +### Async Code in Hooks + +NativeScript CLI supports asynchronous code in hooks. If executing async code, you must return a Promise: + +::: code-group + +```javascript [thehook.mjs (ESM)] +import { mkdirp } from 'mkdirp' + +export default function ($logger) { + return new Promise(function (resolve, reject) { + mkdirp('somedir', function (err) { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} +``` + +```javascript [thehook.js (CommonJS)] +var mkdirp = require('mkdirp') + +module.exports = function ($logger) { + return new Promise(function (resolve, reject) { + mkdirp('somedir', function (err) { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} +``` + +::: + +## Spawned Hooks + +Spawned hooks are executed via Node's `child_process.spawn` from the project's root directory. All options are passed to the script using environment variables: + +| Environment Variable | Description | +| -------------------- | -------------------------------------------------------------------------------------------- | +| `TNS-VERSION` | The version of the NativeScript CLI | +| `TNS-HOOK_FULL_PATH` | The full path to the executed hook | +| `TNS-COMMANDLINE` | The exact command-line arguments passed to NativeScript CLI (e.g., `tns run ios --emulator`) | + +If a spawned hook returns a non-zero exit code, NativeScript CLI will throw an error and abort the command's execution. + +## Hook Types and Behaviors + +**Before/After Hooks** + +Hooks can execute code before or after a specific action: + +::: code-group + +```javascript [thehook.mjs (ESM)] +export default function (hookArgs) { + if (hookArgs.prepareData.release) { + console.log('Before executing release build.') + } +} +``` + +```javascript [thehook.js (CommonJS)] +module.exports = function (hookArgs) { + if (hookArgs.prepareData.release) { + console.log('Before executing release build.') + } +} +``` + +::: + +## Adding Hooks to Plugins + +To add hooks to your plugin, follow these steps: + +**1. Install the Hook Module** + +```bash +npm install @nativescript/hook --save +``` + +**2. Create postinstall.js** + +Create a postinstall script at the root folder of your plugin: +::: code-group + +```javascript [postinstall.mjs (ESM)] +import path from 'path' +import hook from '@nativescript/hook' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +hook(path.join(__dirname, '..')).postinstall() +``` + +```javascript [postinstall.js(CommonJS)] +require('@nativescript/hook')(__dirname).postinstall() +``` + +::: + +**3. Create preuninstall.js** + +Create preuninstall script at the root folder of your plugin: + +::: code-group + +```javascript [postinstall.mjs (ESM)] +import path from 'path' +import hook from '@nativescript/hook' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +hook(path.join(__dirname, '..')).preuninstall() +``` + +```javascript [postinstall.js(CommonJS)] +require('@nativescript/hook')(__dirname).postinstall() +``` + +::: +**4. Update package.json Scripts** + +Add the postinstall and preuninstall scripts: + +```json +{ + "scripts": { + "postinstall": "node postinstall.mjs", + "preuninstall": "node preuninstall.mjs" + } +} +``` + +**5. Declare Hooks in package.json** + +Define your hooks under the `nativescript` property: + +```json +{ + "nativescript": { + "hooks": [ + { + "type": "before-prepare", + "script": "lib/before-prepare.mjs" + }, + { + "type": "after-prepare", + "script": "lib/after-prepare.mjs" + } + ] + } +} +``` + +### Hook Configuration Properties + +#### type (Required) + +Specifies when the hook should execute. Format: `before-` or `after-` + +#### script (Required) + +The relative path from the plugin root to the hook implementation file. + +#### name (Optional) + +Custom name for the hook. Defaults to the plugin package name. + +**Example with custom name:** + +```json +{ + "nativescript": { + "hooks": [ + { + "type": "after-prepare", + "script": "lib/after-prepare.mjs", + "name": "my-custom-hook" + } + ] + } +} +``` + +## Adding Hooks to Applications + +You can define project-persistent hooks in your `nativescript.config.ts` file: + +```typescript +import { NativeScriptConfig } from '@nativescript/core' + +export default { + id: 'org.nativescript.app', + appPath: 'app', + appResourcesPath: 'App_Resources', + hooks: [ + { + type: 'before-prepare', + script: './scripts/hooks/before-prepare.mjs', + }, + { + type: 'after-prepare', + script: './scripts/hooks/after-prepare.mjs', + }, + ], +} as NativeScriptConfig +``` + +## Configuration Reference + +### Hooks Configuration in nativescript.config.ts + +```typescript +hooks: [ + { + type: 'before-' | 'after-', + script: './path/to/script.mjs', + }, +] +``` + +## Available Hook Types + +The following hook types are available (prefix with `before-` or `after-`): + +| Hook Name | Description | Execution Context | +| -------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------ | +| `buildAndroidPlugin` | Builds aar file for Android plugin | Runs during `prepareNativeApp` | +| `buildAndroid` | Builds Android app | During Android build process | +| `buildIOS` | Builds iOS app | During iOS build process | +| `checkEnvironment` | Validates project environment | Runs during `ns doctor`, `ns clean`, and most build commands | +| `checkForChanges` | Detects changes during watch | NativeScript CLI checks application state to decide if rebuild/reinstall/restart is needed | +| `install` | Application installed to device/emulator | After app installation | +| `prepare` | Compiles webpack and prepares native app | Prepares the application in platforms folder | +| `prepareNativeApp` | Prepares the actual native app | Runs during `prepare`/`watch` hook | +| `resolveCommand` | Resolves command and arguments | Runs before all CLI commands | +| `watch` | Sets up watchers for live sync | During `prepare` hook for live development | +| `watchPatterns` | Sets up watch patterns | During `watch` hook | + +### Hook Execution Examples + +**Example: Release Build Check** + +::: code-group + +```javascript [thehook.mjs (ESM)] +export default function (hookArgs) { + if (hookArgs.prepareData.release) { + console.log('Executing release build') + // Modify API keys, enable production features, etc. + } +} +``` + +```javascript [thehook.js (CommonJS)] +module.exports = function (hookArgs) { + if (hookArgs.prepareData.release) { + console.log('Executing release build') + // Modify API keys, enable production features, etc. + } +} +``` + +::: +**Example: Using NativeScript Services** + +::: code-group + +```javascript [thehook.mjs (ESM)] +export default function ($logger, $fs, hookArgs) { + $logger.info('Starting custom hook') + + const configPath = hookArgs.projectData.projectDir + '/config.json' + if ($fs.exists(configPath)) { + const config = $fs.readJson(configPath) + $logger.info(`Loaded config: ${JSON.stringify(config)}`) + } +} +``` + +```javascript [thehook.js (CommonJS)] +module.exports = function ($logger, $fs, hookArgs) { + $logger.info('Starting custom hook') + + const configPath = hookArgs.projectData.projectDir + '/config.json' + if ($fs.exists(configPath)) { + const config = $fs.readJson(configPath) + $logger.info(`Loaded config: ${JSON.stringify(config)}`) + } +} +``` + +::: + +## Hooks CLI Command + +As described above these hooks are installed automatically through npm postinstall scripts included in the plugin package. + +However, if you (or your CI environment) install dependencies with: + +```bash +npm install --ignore-scripts +``` + +then those postinstall scripts don’t run, which means: + +- Plugin hooks aren’t copied to the correct location (`hooks/` folder). +- Builds may fail or certain plugin functionality won’t work. + +Starting with NativeScript 9.0 CLI (`npm install -g nativescript`), a new command was introduced: + +```bash +ns hooks +``` + +This command installs all plugin hooks after dependencies have been installed. + +So if your environment blocks postinstall scripts, you can now safely do: + +```bash +npm install --ignore-scripts +ns hooks +``` + +The new `ns hooks` command will install the hooks into the proper project locations (as postinstall would have done). + +**Available Commands** + +| Command | Description | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ns hooks` | Lists the hooks that are in the installed plugins ( also `ns hooks list` ). | +| `ns hooks install` | Installs the hooks. | +| `ns hooks lock` | Creates a `nativescript-lock.json` file, this is a list of hooks per plugin and a hash of the script for each hook. | +| `ns hooks verify` | Compares the hooks in the plugins with what is specified in the `nativescript-lock.json` file, failing if a hook is not listed or the hash is not the same. | + +**For extra peace of mind** + +Typically the contents of the hook scripts do not change in plugins from version to version, to prevent unexpecected changes to hooks you can utilize the `lock` command. + +This will: + +- Create a `nativescript-lock.json` file containing details of the current plugin hooks. +- Ensure that any future `ns hooks install` or `ns hooks verify` invocations will fail if any new hooks are introduced by dependencies or if these hook scripts differ from what was previously installed. + +i.e. run `ns hooks lock` and `ns hooks install` will fail if any of the hooks have changed since the last time `ns hooks lock` has been executed. diff --git a/content/guide/multi-window.md b/content/guide/multi-window.md new file mode 100644 index 00000000..b747b781 --- /dev/null +++ b/content/guide/multi-window.md @@ -0,0 +1,244 @@ +--- +title: Multiple Windows +description: Develop with multiple windows on supported devices. +contributors: + - NathanWalker +--- + +NativeScript 9 adds first-class support for iOS multi-window (multi-scene) applications by adopting the UIScene lifecycle when enabled. This guide explains how to enable scenes, how NativeScript integrates with them, and how to work with scene-specific APIs and events. + +:::tip Why this matters +Apple is moving all iOS apps to the UIScene lifecycle. Enabling scenes now makes your app future‑proof and unlocks multiple windows on iPadOS and visionOS. +::: + +## Supported platforms + +- iPad running iPadOS 13 or later (multi-window capable) +- visionOS (Vision Pro) +- iPhone: runs with UIScene lifecycle; multiple windows are not exposed to users, but adopting UIScene is recommended + +## Prerequisites + +- NativeScript 9+ +- iOS 13+ runtime +- Xcode/iOS tooling capable of building with UIScene (Xcode 11+) + +## Enable scene lifecycle (Info.plist) + +NativeScript will automatically switch to UIScene lifecycle when a scene manifest is present in your iOS app `Info.plist`. Add the following keys: + +```xml +UIApplicationSceneManifest + + UIApplicationPreferredDefaultSceneSessionRole + UIWindowSceneSessionRoleApplication + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + SceneDelegate + + + + +``` + +When this configuration is detected, NativeScript adopts UIScene; on devices that don’t support scenes, your app continues to behave as a single-window app. + +:::note iPhone and UIScene +Even on iPhone, adding the manifest switches your app to UIScene lifecycle. Xcode may show warnings like “UIScene lifecycle will soon be required” — using the manifest addresses this. +::: + +## How it works in NativeScript + +When the scene manifest is present: + +- A `SceneDelegate` (exposed to iOS as `SceneDelegate`) implements `UIWindowSceneDelegate` to integrate scenes with NativeScript’s application runtime. +- A UIWindow is created per `UIWindowScene` and mapped internally, preserving compatibility with traditional app lifecycle APIs. +- NativeScript fires scene-specific events and forwards core application lifecycle events from a primary scene to maintain compatibility with existing code. + +### Scene lifecycle events + +Use the `SceneEvents` constants to subscribe to scene lifecycle changes: + +```ts +export const SceneEvents = { + sceneWillConnect: 'sceneWillConnect', + sceneDidActivate: 'sceneDidActivate', + sceneWillResignActive: 'sceneWillResignActive', + sceneWillEnterForeground: 'sceneWillEnterForeground', + sceneDidEnterBackground: 'sceneDidEnterBackground', + sceneDidDisconnect: 'sceneDidDisconnect', + sceneContentSetup: 'sceneContentSetup', +}; +``` + +Event payloads include scene and window references: + +```ts +/** iOS event data for UIScene lifecycle (iOS 13+). */ +export interface SceneEventData extends ApplicationEventData { + /** The UIWindowScene instance associated with this event. */ + scene?: UIWindowScene; + /** The UIWindow for this scene (if applicable). */ + window?: UIWindow; + /** Scene connection options (for sceneWillConnect). */ + connectionOptions?: UISceneConnectionOptions; + /** Additional user info from the notification. */ + userInfo?: NSDictionary; +} +``` + +### iOSApplication scene APIs + +When UIScene is active, `Application.ios` exposes helpers for inspecting and controlling scenes and windows: + +- `supportsScenes(): boolean` — iOS supports UIScene (iOS 13+) +- `supportsMultipleScenes(): boolean` — app can present multiple scenes/windows (iPadOS; typically false on iPhone and some simulators) +- `getAllWindows(): UIWindow[]` — all app windows across scenes +- `getAllScenes(): UIScene[]` — all attached scenes +- `getWindowScenes(): UIWindowScene[]` — filtered to window scenes +- `getPrimaryWindow(): UIWindow | undefined` — the primary window (for compatibility) +- `getPrimaryScene(): UIWindowScene | undefined` — the primary scene +- `isUsingSceneLifecycle(): boolean` — whether UIScene lifecycle is active +- `setWindowRootView(window: UIWindow, view: View): void` — set NativeScript root view for a given scene’s window + +## Usage + +### Listen to scene events + +```ts +import { Application, SceneEvents } from '@nativescript/core'; + +Application.on(SceneEvents.sceneWillConnect, (args) => { + console.log('Scene connecting:', args.scene); + console.log('Window:', args.window); + console.log('Connection options:', args.connectionOptions); +}); + +Application.on(SceneEvents.sceneDidActivate, (args) => { + console.log('Scene active:', args.scene); +}); + +Application.on(SceneEvents.sceneWillResignActive, (args) => { + console.log('Scene will resign active:', args.scene); +}); + +Application.on(SceneEvents.sceneWillEnterForeground, (args) => { + console.log('Scene will enter foreground:', args.scene); +}); + +Application.on(SceneEvents.sceneDidEnterBackground, (args) => { + console.log('Scene entered background:', args.scene); +}); + +Application.on(SceneEvents.sceneDidDisconnect, (args) => { + console.log('Scene disconnected:', args.scene); +}); + +Application.on(SceneEvents.sceneContentSetup, (args) => { + // Create and attach NativeScript View content for the new scene here + // See "Provide scene-specific UI" section below + setupSceneContent(args); +}); +``` + +### Inspect and manage windows + +```ts +import { Application } from '@nativescript/core'; + +if (Application.ios.supportsScenes()) { + const windows = Application.ios.getAllWindows(); + const scenes = Application.ios.getWindowScenes(); + const primaryWindow = Application.ios.getPrimaryWindow(); + + console.log(`App has ${windows.length} windows`); + console.log(`App has ${scenes.length} scenes`); + console.log('Primary window:', primaryWindow); + + if (Application.ios.isUsingSceneLifecycle()) { + console.log('Using UIScene lifecycle'); + } +} else { + console.log('Single-window app lifecycle in effect'); +} +``` + +### Provide scene-specific UI + +```ts +import { Application, Page, Utils } from '@nativescript/core'; + +function createPageForScene(scene: UIWindowScene, window: UIWindow): Page { + // Construct any NativeScript view hierarchy here + const page = new Page(); + // ... add content + return page; +} + +export function setupSceneContent(args: SceneEventData) { + // Optionally distinguish scenes by an id when opening a new window + // (e.g., via NSUserActivity userInfo) + let nsViewId: string | undefined; + if (args.connectionOptions?.userActivities?.count > 0) { + const activity = args.connectionOptions.userActivities.allObjects.objectAtIndex(0) as NSUserActivity; + nsViewId = Utils.dataDeserialize(activity.userInfo).id; + } + + let page: Page; + switch (nsViewId) { + case 'newSceneBasic': + page = createPageForScene(args.scene, args.window); + break; + case 'newSceneAlt': + page = createPageForScene(args.scene, args.window); // replace with alt page + break; + default: + page = createPageForScene(args.scene, args.window); + } + + Application.ios.setWindowRootView(args.window, page); +} +``` + +## Custom SceneDelegate (advanced) + +NativeScript ships a default `SceneDelegate` that integrates UIScene with the runtime and event system. If your application needs custom scene delegate behavior, you can provide your own implementation named `SceneDelegate` in your app and wire additional logic. Ensure that you continue to create a `UIWindow` per `UIWindowScene` and set the NativeScript root view to keep app behavior consistent. Most apps should prefer the default delegate. + +## Compatibility and behavior + +- Backwards compatibility: on devices or builds without a scene manifest, the traditional single-window lifecycle is used and existing apps continue to work unchanged. +- Primary scene: for compatibility, NativeScript forwards core app lifecycle events (e.g., didBecomeActive) from the primary scene. +- Multiple scenes: `supportsMultipleScenes()` is typically only true on physical iPadOS devices; it may return false on iPhone and some simulators. + +## Migration guidance + +Existing apps do not need to change code to adopt UIScene. To enable multi-window capabilities and Scene events: + +1. Add the scene manifest to `Info.plist` (see above). +2. Listen to `SceneEvents` to tailor behavior per window. +1. If you open additional windows, set their root views with `Application.ios.setWindowRootView` during `sceneContentSetup`. + +## Troubleshooting + +- “UIScene lifecycle will soon be required” in Xcode: add the scene manifest to `Info.plist` to adopt UIScene. +- No multiple windows on iPhone: expected; iPhone uses UIScene lifecycle but doesn’t expose multi-window UX to users. +- `supportsMultipleScenes()` returns false on simulator: test on a physical iPad where multi-window is supported. + +## Summary + +With UIScene enabled, NativeScript gives you: + +- Scene-aware events for window lifecycle handling +- APIs to inspect scenes and windows and set scene-specific root views +- Backwards-compatible behavior for apps that haven’t yet adopted scenes + +Use the examples above as a starting point to build multi-window workflows on iPadOS and visionOS while keeping your app ready for the future UIScene requirement on iOS. + diff --git a/content/guide/styling.md b/content/guide/styling.md index 4a8a642c..afc353ac 100644 --- a/content/guide/styling.md +++ b/content/guide/styling.md @@ -226,7 +226,8 @@ This list of properties can be set in CSS or through the style property of each | `border-radius` | `borderRadius` | Sets a border radius to the matched view’s. | | `box-shadow` | `boxShadow` | Sets a box shadow to the matched view's. | | `clip-path` | `clipPath` | Sets the clip-path. Supported shapes are circle, ellipse, rect and polygon. You can define your own shape using [clippy](http://bennettfeely.com/clippy/) | -| `color` | `color` | Sets a solid-color value to the matched view’s foreground. | +| `color` | `color` | Sets a solid-color value to the matched view’s foreground. +| `direction` | `direction` | Sets the direction of text, table and grid columns, and horizontal overflow. Use rtl for languages written from right to left (like Hebrew or Arabic), and ltr for those written from left to right (like English and most other languages). | | `font` | `font` | Sets the font properties (this includes `font-family`, `font-size`, `font-style` and `font-weight`) of the matched view. | | `font-family` | `fontFamily` | Sets the font family of the matched view. | | `font-size` | `fontSize` | Sets the font size of the matched view (only supports device-independent units). | @@ -259,6 +260,39 @@ This list of properties can be set in CSS or through the style property of each | `width` | `width` | Sets the view width. | | `z-index` | `zIndex` | Sets the z-index. (On Android API Level 21 and above.) | +## Layout direction (LTR / RTL) + +NativeScript now supports an **inherited CSS `direction` property** to force the layout direction of views to either left-to-right (`ltr`) or right-to-left (`rtl`). + +```css +/* Apply RTL to the entire app */ +.ns-root { + direction: rtl; +} +``` + +What this does: + +- tells NativeScript to use the **platform’s native direction APIs** so you get the OS-level behavior for RTL, +- makes navigation transitions direction-aware (slide, flip, etc.), +- makes default label/text alignment respect the direction, +- enables direction-aware layout for FlexboxLayout on iOS, +- aligns horizontal scroll views to the end in RTL, +- and allows using `start` / `end` values for `horizontalAlignment` to align relative to the current direction. + +Android note: + +```xml + + + +``` + +You **must** enable `android:supportsRtl="true"` in the manifest for Android to actually honor RTL. On iOS this is always available. + ### Accessing NativeScript View properties with CSS You can also set NativeScript component properties value that are not part of the CSS specification. For example: diff --git a/content/sidebar.ts b/content/sidebar.ts index eec69433..09ed37b7 100644 --- a/content/sidebar.ts +++ b/content/sidebar.ts @@ -151,6 +151,10 @@ export default [ text: 'Config Reference', link: '/configuration/nativescript', }, + { + text: 'Vite Reference', + link: '/configuration/vite', + }, { text: 'Webpack Reference', link: '/configuration/webpack', @@ -312,6 +316,10 @@ export default [ }, ], }, + { + text: 'Hooks', + link: '/guide/hooks', + }, { text: 'Platform Version Handling', link: '/guide/platform-version-handling', diff --git a/content/ui/list-view.md b/content/ui/list-view.md index 575f1d25..65259b7a 100644 --- a/content/ui/list-view.md +++ b/content/ui/list-view.md @@ -1,18 +1,19 @@ --- title: ListView -description: UI component for rendering large lists using view recycling. +description: UI component for rendering large lists using view recycling, with optional sticky headers, sectioned data, and an integrated search bar. contributors: - rigor789 - Ombuweb + - NathanWalker --- -`` is a UI component that renders items in a vertically scrolling list, the template for the items can be defined via `itemTemplate` (or multiple templates via `itemTemplates` - more on that below). The ListView only renders the visible items, as the user scrolls, new items render by reusing a no-longer-visible item's view—this is usually referred to as view-recycling. +`` is a UI component that renders items in a vertically scrolling list. The template for the items can be defined via `itemTemplate` (or multiple templates via `itemTemplates` — more on that below). The ListView only renders the visible items; as the user scrolls, new items render by reusing a no-longer-visible item's view — this is usually referred to as view-recycling. -::: tip +Newer versions of `ListView` (v9+) can also render **sectioned data** (for example, A–Z lists) with **sticky headers** and an **optional search bar** that can auto-hide on iOS. -For additional features and improved performance in certain scenarios, consider using an alternative implementation like the +::: tip +You can also explore [CollectionView](https://github.com/nativescript-community/ui-collectionview) from the community. - ::: @@ -78,6 +79,72 @@ Individual items can be rendered using a different template. For example, let's +### Sectioned ListView with sticky headers and search + +Starting with v9, ListView can render sectioned data with sticky headers and an optional search bar. This is useful for contact lists, country lists, or any data grouped by a key. + +A supported data shape looks like this: + +```ts +const countries: { title: string; items: { name: string; code?: string; flag?: string }[] }[] = [ + { + title: 'A', + items: [ + { name: 'Albania', code: 'AL' }, + ], + }, + { + title: 'B', + items: [ + { name: 'Bahamas', code: 'BS' }, + ], + }, +] +``` + +You can then bind this data to the ListView and enable the new props: + +```xml + +``` + +In code you can listen for search changes: + +```ts +listView.on('searchChange', (args: SearchEventData) => { + console.log('search text:', args.text) + // apply filtering to your backing data if desired +}) +``` + +Notes: + +- `searchAutoHide` is currently iOS-only (it will auto-hide the search on scroll). +- `stickyHeaderTemplate` accepts the same binding context as the section (`title`, etc.). +- Make sure each section’s `items` array is present; empty / null sections may not render as expected. + ## Props ### items @@ -128,6 +195,71 @@ Gets or sets the available itemTemplates. See [KeyedTemplate](/api/interface/KeyedTemplate). +### sectioned + +```ts +sectioned: boolean +``` + +Enables sectioned data rendering on the `ListView`. When `true`, the ListView expects the `items` source to be an **array of sections** where each section has a `title` (or similar field) and an `items` array: + +```ts +{ + title: string; + items: any[]; +} +``` + +This allows the ListView to render grouped lists with headers. + +### stickyHeader + +```ts +stickyHeader: boolean +``` + +Enables sticky (pinned) headers while scrolling sectioned data. When enabled, the current section header stays at the top of the list until the next section header pushes it away. + +### stickyHeaderTemplate + +```ts +stickyHeaderTemplate: string | KeyedTemplate +``` + +Gets or sets the template used to render a section header when sticky headers are enabled. This accepts bindings from the current section (for example: `{{ title }}`). + +### stickyHeaderHeight + +```ts +stickyHeaderHeight: number +``` + +Explicit height for the sticky header. Providing this can improve measurement and scrolling performance, especially on iOS where headers update as you scroll. + +### stickyHeaderTopPadding + +```ts +stickyHeaderTopPadding: boolean | number +``` + +Controls the padding applied to the sticky header at the top. Set to `false` to disable the extra top padding; set to a number to supply an explicit padding value. + +### showSearch + +```ts +showSearch: boolean +``` + +Shows a built-in search bar above the ListView. This is useful when you want a declarative, per-list search input without adding a separate `SearchBar` component. + +### searchAutoHide + +```ts +searchAutoHide: boolean +``` + +(iOS only) When `true`, the built-in search bar will auto-hide when the user scrolls. This mirrors common iOS list behaviors. + ### separatorColor ```ts @@ -150,6 +282,8 @@ rowHeight: number Gets or sets the row height of the ListView. Useful when your items have a fixed height, as the required calculations are greatly simplified and the rendering can be faster. +> Android: with the latest ListView improvements, row items will now react properly to spacing (padding and margin), so setting `rowHeight` alongside your layout spacing should behave more predictably across platforms. + ### iosEstimatedRowHeight Gets or sets the estimated height of rows in the ListView. Default value: `44px` @@ -235,6 +369,18 @@ on('loadMoreItems', (args: EventData) => { Emitted when the user reaches the end of the ListView. Useful for loading additional items (ie. infinite scroll). +### searchChange + +```ts +on('searchChange', (args: SearchEventData) => { + console.log('Search text changed:', args.text) +}) +``` + +Emitted when the built-in search bar text changes. You can use this to filter the underlying list data, or to drive remote searches. + +See `SearchEventData` in the API reference for the shape of the event. + ## Native component - Android: [`android.widget.ListView`](https://developer.android.com/reference/android/widget/ListView.html) diff --git a/content/ui/page.md b/content/ui/page.md index 5eb18771..684e33be 100644 --- a/content/ui/page.md +++ b/content/ui/page.md @@ -267,6 +267,44 @@ Gets or sets the color of the status bar on Android devices. **Android only.** See [Color](/api/class/Color). +### androidOverflowEdge + +```ts +androidOverflowEdge: + | 'none' + | 'left' + | 'top' + | 'right' + | 'bottom' + | 'dont-apply' + | 'left-dont-consume' + | 'top-dont-consume' + | 'right-dont-consume' + | 'bottom-dont-consume' + | 'all-but-left' + | 'all-but-top' + | 'all-but-right' + | 'all-but-bottom' +``` + +Controls how Android system insets (status bar, navigation bar, cutouts) are applied and/or consumed by the Page. When insets are applied, they are added to the Page's padding. Insets propagate down the view hierarchy until consumed. Defaults to `'dont-apply'` for Pages. **Android only.** + +Options: + +| Value | Behavior | +|---|---| +| `none` | Apply and consume all inset edges | +| `left` / `top` / `right` / `bottom` | Apply and consume only the specified edge | +| `dont-apply` | Do not apply or consume any insets — triggers `androidOverflowInset` | +| `left-dont-consume` | Apply the left inset but do not consume it; all other insets are ignored | +| `top-dont-consume` | Apply the top inset but do not consume it; all other insets are ignored | +| `right-dont-consume` | Apply the right inset but do not consume it; all other insets are ignored | +| `bottom-dont-consume` | Apply the bottom inset but do not consume it; all other insets are ignored | +| `all-but-left` | Apply and consume all insets except left | +| `all-but-top` | Apply and consume all insets except top | +| `all-but-right` | Apply and consume all insets except right | +| `all-but-bottom` | Apply and consume all insets except bottom | + ### enableSwipeBackNavigation ```ts @@ -346,6 +384,72 @@ on('navigatedFrom', (args: NavigatedData) => { Emitted after the app has navigated away from the current page. +### androidOverflowInset + +```ts +on('androidOverflowInset', (args) => { + // args.inset: { top, bottom, left, right, topConsumed?, bottomConsumed?, leftConsumed?, rightConsumed? } +}) +``` + +Emitted when `androidOverflowEdge` is set to `'dont-apply'`, allowing manual handling of system insets. You can inspect and modify inset values and explicitly consume individual sides by setting the corresponding `*Consumed` flags. **Android only.** + +Example: + +```ts +page.on('androidOverflowInset', (args) => { + // Modify inset values if needed + args.inset.top += 10 + args.inset.bottom += 10 + args.inset.left += 10 + args.inset.right += 10 + + // Explicitly consume each side + args.inset.topConsumed = true + args.inset.bottomConsumed = true + args.inset.leftConsumed = true + args.inset.rightConsumed = true +}) +``` + +## Android: Edge-to-Edge tip + +::: tip Edge-to-Edge on Android +You can opt into full edge-to-edge and precisely control how system insets are handled on a per-Page basis. + +- Set `androidOverflowEdge` to choose which inset edges to apply and/or consume. Pages default to `'dont-apply'`. +- When using `'dont-apply'`, handle `androidOverflowInset` to adjust and explicitly consume sides. +- Call `Utils.android.enableEdgeToEdge(...)` to enable edge-to-edge and configure light/dark system UI overlays. + +Example: + +```ts +import { Utils } from '@nativescript/core' + +// Let the page handle insets manually +page.androidOverflowEdge = 'dont-apply' + +// Enable edge-to-edge (Android) with light/dark colors +import { Application, Color } from '@nativescript/core' +const activity = + Application.android.foregroundActivity || Application.android.startActivity +Utils.android.enableEdgeToEdge(activity, { + statusBarLightColor: new Color('#FFFFFF'), + statusBarDarkColor: new Color('#000000'), +}) + +// Optionally handle and consume insets yourself +page.on('androidOverflowInset', (args) => { + args.inset.top += 8 + args.inset.bottom += 8 + args.inset.topConsumed = true + args.inset.bottomConsumed = true +}) +``` + +See also: [enableEdgeToEdge](/core/utils#enableedgetoedge). +::: + ## Native component - Android: [`org.nativescript.widgets.GridLayout`](https://github.com/NativeScript/NativeScript/blob/master/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/GridLayout.java) diff --git a/content/ui/sidebar.ts b/content/ui/sidebar.ts index 58748f73..5524ee7b 100644 --- a/content/ui/sidebar.ts +++ b/content/ui/sidebar.ts @@ -70,6 +70,7 @@ export default [ { text: 'SearchBar', link: '/ui/search-bar' }, { text: 'SegmentedBar', link: '/ui/segmented-bar' }, { text: 'Slider', link: '/ui/slider' }, + { text: 'SplitView', link: '/ui/split-view' }, { text: 'Switch', link: '/ui/switch' }, { text: 'TabView', link: '/ui/tab-view' }, { text: 'TextField', link: '/ui/text-field' }, diff --git a/content/ui/split-view.md b/content/ui/split-view.md new file mode 100644 index 00000000..754ecdd9 --- /dev/null +++ b/content/ui/split-view.md @@ -0,0 +1,225 @@ +--- +title: SplitView +description: A singular root view component for coordinating up to four column roles (primary, secondary, supplementary, inspector). +contributors: + - NathanWalker +--- + +`` is an iOS-only container component that gives you a declarative [UISplitViewController](https://developer.apple.com/documentation/uikit/uisplitviewcontroller) and exposes its modern multi-column capabilities to NativeScript apps. It lets you coordinate **up to four** roles: + +- **primary** – main navigation or master content +- **secondary** – detail view for the selected item +- **supplementary** – contextual / side content +- **inspector** – tool / inspector pane available on iOS 17+ when the system supports it + +Each role is typically provided by a child `` so that every column can manage its own navigation stack independently, while still being part of the same split view controller. This mirrors how native iPadOS apps structure complex layouts. + +::: tip +It is intended to be used as the singular starting root view for the entire app. + +This component is ideal for iPadOS-style apps, admin-style layouts, or any experience where you need a master-detail flow plus an extra contextual pane or inspector. +::: + +## Example + +### Declarative XML + +```xml + + + + + + + +``` +This configures a 3–4 column layout (depending on OS support) and assigns each role to its own frame. + +### Programmatic + +```ts +import { SplitView } from '@nativescript/core' + +const splitView = new SplitView() +splitView.displayMode = 'twoBesideSecondary' +splitView.splitBehavior = 'tile' +splitView.preferredPrimaryColumnWidthFraction = 0.25 + +// create frames with splitRole before adding them +``` + +### With inspector change listener + +```ts +splitView.on('inspectorChange', ({ data }) => { + console.log('Inspector visible?', data.showing) +}) +``` +Fired whenever the inspector column is shown or hidden. + +## Concepts + +### Roles + +SplitView coordinates **roles** rather than arbitrary children. Children declare their intended role: + +```xml + + + + +``` + +If a role is omitted, SplitView falls back to the order in which children were added. However, declaring the role is recommended for clarity and for future layout changes. + +### Column styles + +The underlying controller can operate in *double* or *triple* column styles. SplitView exposes this through a common enum: + +- `SplitView.SplitStyle.double` +- `SplitView.SplitStyle.triple` + +iOS chooses the right `UISplitViewControllerStyle` for you. On iOS 17+ an inspector column can also be shown. citeturn2view0 + +## Props + +### displayMode + +```ts +displayMode: + | 'automatic' + | 'secondaryOnly' + | 'oneBesideSecondary' + | 'oneOverSecondary' + | 'twoBesideSecondary' + | 'twoOverSecondary' + | 'twoDisplaceSecondary' +``` + +Maps to [UISplitViewController.preferredDisplayMode]. Determines how the primary/supplementary columns relate to the secondary column (beside vs over vs displaced). + +### splitBehavior (iOS 14+) + +```ts +splitBehavior: 'automatic' | 'tile' | 'overlay' | 'displace' +``` + +Maps to [UISplitViewController.preferredSplitBehavior](https://developer.apple.com/documentation/uikit/uisplitviewcontroller/preferreddisplaymode). Controls how columns behave when the size class changes (for example overlaying instead of resizing). iOS 14 or newer is required. + +### preferredPrimaryColumnWidthFraction + +```ts +preferredPrimaryColumnWidthFraction: number // 0..1 +``` + +Fractional width for the primary column (for example `0.25` = 25%). + +### preferredSupplementaryColumnWidthFraction + +```ts +preferredSupplementaryColumnWidthFraction: number // 0..1 +``` + +Used in triple-column style to reserve width for the supplementary column. + +### preferredInspectorColumnWidthFraction + +```ts +preferredInspectorColumnWidthFraction: number // 0..1 +``` + +Width fraction for the inspector column, applied only when the platform supports the inspector role (iOS 17+). On earlier versions this is safely ignored. + +### inspectorShowing (readonly) + +```ts +inspectorShowing: boolean +``` + +Current visibility state of the inspector column. Helpful to sync UI buttons with the actual split view state. + +## Static members + +### SplitView.getInstance() + +```ts +const active = SplitView.getInstance() +``` + +Returns the last created SplitView instance, or `null` if none exists. This is a convenience for apps that only ever use a single SplitView and want to access it imperatively (for example, from a service). + +### SplitView.SplitStyle + +```ts +const { SplitStyle } = SplitView +``` + +Common enum representing double vs triple column styles. The iOS implementation maps this to the proper `UISplitViewController` style. + +## Methods + +### showPrimary() / hidePrimary() + +```ts +splitView.showPrimary() +splitView.hidePrimary() +``` + +Shows or hides the primary column. Useful on compact width or when presenting master-detail flows modally. + +### showSecondary() / hideSecondary() + +```ts +splitView.showSecondary() +splitView.hideSecondary() +``` + +Controls the visibility of the secondary column. + +### showSupplementary() + +```ts +splitView.showSupplementary() +``` + +Ensures the supplementary column is visible when the display mode allows it. Hiding is typically handled automatically by the display mode. + +### showInspector() / hideInspector() + +```ts +splitView.showInspector() +splitView.hideInspector() +``` + +Toggles the inspector column (iOS 17+). Calls are no-ops on platforms/versions where inspector is not available. + +### onSecondaryViewCollapsed(secondaryVC, primaryVC) + +Lifecycle hook invoked when the system collapses the secondary onto the primary (for example, when moving to a compact size). Override in subclasses or listen at the NativeScript level to adjust your UI. + +## Events + +### inspectorChange + +```ts +splitView.on('inspectorChange', (args) => { + console.log('Inspector visible?', args.data.showing) +}) +``` + +Emitted whenever the inspector column changes visibility. Payload contains a `data.showing: boolean`. + +## Platform notes + +- **Platform**: iOS only (backed by `UISplitViewController`). +- **splitBehavior**: requires iOS 14+. +- **Inspector**: requires iOS 17+. On earlier versions the property is ignored and inspector-related methods are safe no-ops. + +## Native component + +- iOS: [`UISplitViewController`](https://developer.apple.com/documentation/uikit/uisplitviewcontroller) diff --git a/content/ui/tab-view.md b/content/ui/tab-view.md index ed13bb7f..b7f53c81 100644 --- a/content/ui/tab-view.md +++ b/content/ui/tab-view.md @@ -1,13 +1,18 @@ --- title: TabView -description: UI component for grouping content into tabs and let users switch between them. +description: UI component for grouping content into tabs and letting users switch between them. contributors: - rigor789 - Ombuweb + - NathanWalker --- `` is a UI component that shows content grouped into tabs and lets users switch between them. +As of NativeScript 9, on **iOS 26+**, TabView now supports: +- an **optional bottom accessory view** (`iosBottomAccessory`) that sits just above the tab bar and participates in layout, and +- a **configurable tab bar minimize behavior** (`iosTabBarMinimizeBehavior`) so you can control how/when the tab bar hides when scrolling. + @@ -130,7 +135,7 @@ Sets the underline color of the tabs. **Android only.** ```css .tab-view { - android-selected-tab-highlight-color:: #3d5a80; + android-selected-tab-highlight-color: #3d5a80; } ``` @@ -160,6 +165,38 @@ Defaults to `automatic`. See [UIImage.RenderingMode](https://developer.apple.com/documentation/uikit/uiimage/renderingmode). +### iosBottomAccessory + +```ts +iosBottomAccessory: View // iOS 26+ only +``` + +Assigns a bottom accessory view that is rendered *above* the iOS tab bar, inside the TabView's layout. This is useful for mini players, status bars, or context-sensitive actions that should stay attached to the tab bar. On platforms below iOS 26 this property is ignored. + +Notes: +- Give it an explicit `height` or style it with CSS so the TabView can measure it. +- It participates in layout pass fixes added in this release so it will resize alongside safe areas. +- On Android this is ignored. + +### iosTabBarMinimizeBehavior + +```ts +iosTabBarMinimizeBehavior: + | 'automatic' + | 'never' + | 'onScrollDown' + | 'onScrollUp' +``` + +Controls how the iOS tab bar minimizes/hides in response to scrolling. This mirrors the iOS 26 tab bar behavior on `UITabBarController`. + +- `automatic` – system chooses; good default. +- `never` – keep the tab bar always visible. +- `onScrollDown` – hide the tab bar when scrolling down. +- `onScrollUp` – show the tab bar when scrolling up. + +Ignored on iOS < 26 and on Android. + ### ...Inherited For additional inherited properties, refer to the [API Reference](/api/class/TabView). @@ -194,7 +231,7 @@ iconSource: string Gets or sets the icon source of the tab strip entry. -Supported paths are `res://` or an absolute path (eg. `~/assets/image.png`). +Supported paths are `font://`, `res://`, `sys://` (iOS only), or an absolute path (eg. `~/assets/image.png`). See [`Image`](/ui/image) for details on the different paths. @@ -211,6 +248,45 @@ on('selectedIndexChanged', (args: EventData) => { Emitted when the selected tab changes. +## Platform specific notes + +### iOS 26+ bottom accessory + +On iOS 26+ you can attach any NativeScript view as a bottom accessory, such as a mini player, quick actions, or a context bar. On earlier iOS versions and on Android this is ignored safely. + +```xml + + + + + + +``` + +You can also set the accessory from code if you need to build it dynamically: + +```ts +const tabView = page.getViewById('mainTabs') as TabView +const accessory = new StackLayout() +accessory.height = 44 +accessory.backgroundColor = '#1c1c1e' +tabView.iosBottomAccessory = accessory +``` + ## Native component - Android: [`androidx.viewpager.widget.ViewPager`](https://developer.android.com/reference/androidx/viewpager/widget/ViewPager)