Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions resources/views/docs/mobile/4/digging-deeper/lifecycle-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,51 @@ class Dashboard extends NativeComponent

</aside>

## The app going to the background

<x-docs.version-badge since="4.8" />

The hooks above follow screens. To react when the whole app leaves the foreground and comes back, listen for the
`AppBackgrounded` and `AppForegrounded` events:

```php
use Native\Mobile\Attributes\On;
use Native\Mobile\Events\App\AppBackgrounded;
use Native\Mobile\Events\App\AppForegrounded;

#[On(AppBackgrounded::class)]
public function backgrounded(): void
{
// Save a draft. The OS may suspend PHP right after this.
}

#[On(AppForegrounded::class)]
public function foregrounded(int $secondsAway): void
{
// Away for more than five minutes? Reload anything that may be stale.
}
```

- `AppBackgrounded` fires when the user goes home, switches apps, or locks the device. The OS may suspend PHP right
after it, so save what must survive and don't start slow work.
- `AppForegrounded` fires when the app comes back. `$secondsAway` is measured natively on a clock that keeps counting
while the device sleeps, which makes it a good fit for timeouts such as locking the app or refreshing stale data.
A cold launch does not fire it.

Returning to the app doesn't call `onResume()` — that only runs when a screen is shown again after the one above it
is popped. `AppForegrounded` is the hook for "the user is back".

<aside>

On Android, opening another app's activity also counts as going to the background, including the system camera and
an in-app browser (a Chrome Custom Tab), because they run in their own processes. On iOS, the image picker and in-app
browser run inside your app, so they don't fire these events.

</aside>

Both events [broadcast globally](../the-basics/events#broadcasting-globally), so you can also listen anywhere in the
app with `Event::listen(AppBackgrounded::class, ...)`.

## Observing the lifecycle from outside

<x-docs.version-badge since="4.1" />
Expand Down
4 changes: 4 additions & 0 deletions resources/views/docs/mobile/4/edge-components/bottom-sheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ Per Material 3, the container color resolves from `theme.surface`. For a custom
```
@endverbatim

Handlers like `@press="$show = true"` and `@dismiss="$show = false"` set the property directly — no component method
needed <x-docs.version-badge since="4.8" />. Keep the property in sync on `@dismiss` so the sheet opens again the
next time you set it to `true`. See [Handlers](introduction#handlers) for the full syntax.

## Props

- `visible` - Whether the sheet is shown (required, boolean)
Expand Down
34 changes: 34 additions & 0 deletions resources/views/docs/mobile/4/edge-components/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,40 @@ Boolean attributes follow PHP truthiness. A bare attribute (`disabled`) or a bou
as you'd expect, but a literal string such as `disabled="false"` is a non-empty string and counts as `true`. Bind
booleans with `:` whenever the value can be false.

## Handlers

Event attributes such as `@press`, `@tap`, `@change`, and `@dismiss` take a handler. A handler is one of:

- a method name: `@press="save"`
- a method call with literal arguments: `@press="selectItem(@{{ $item->id }})"`
- an assignment to a public property <x-docs.version-badge since="4.8" />: `@press="$showSheet = true"`
- a bool toggle <x-docs.version-badge since="4.8" />: `@press="$expanded = !$expanded"`

@verbatim
```blade
<native:button label="Open" @press="$showSheet = true" />

<native:bottom-sheet :visible="$showSheet" @dismiss="$showSheet = false">
{{-- ... --}}
</native:bottom-sheet>

<native:pressable @press="$tab = 'settings'">
<native:text>Settings</native:text>
</native:pressable>
```
@endverbatim

The assigned value must be a literal: `true`, `false`, `null`, a number, or a quoted string. Nothing is evaluated, so
`$open = foo()`, `$open = $other`, and `$open = !$other` don't run. A toggle only works on a property that holds a
bool. Assignments go through the same state pipeline as [data binding](../digging-deeper/data-binding), so
`#[Locked]` properties are refused and the `updating` / `updated` hooks fire. The same syntax works in a
[nested component's](../the-basics/nested-components#events-up) event binding.

A handler that can't run is never dropped silently. With `app.debug` on, it throws
`Native\Mobile\Edge\Exceptions\InvalidHandlerException`, and the error screen names the handler and the component.
With debug off, it's logged and nothing runs. That covers a method the component doesn't have, an expression handlers
don't support, a property that isn't public, and a toggle on a property that isn't a bool.

## The `native:` prefix is optional

`<native:column>` and `<column>` compile to exactly the same thing — the prefix is optional. We use it throughout
Expand Down
4 changes: 4 additions & 0 deletions resources/views/docs/mobile/4/edge-components/modal.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ Per Material 3, backdrop and surface colors come from `theme.background`. The cl
```
@endverbatim

Handlers like `@press="$show = true"` and `@dismiss="$show = false"` set the property directly — no component method
needed <x-docs.version-badge since="4.8" />. Keep the property in sync on `@dismiss` so the modal opens again the
next time you set it to `true`. See [Handlers](introduction#handlers) for the full syntax.

## Props

- `visible` - Whether the modal is shown (required, boolean)
Expand Down
29 changes: 26 additions & 3 deletions resources/views/docs/mobile/4/edge-components/top-bar.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ On iOS, if more than 5 actions are provided, they collapse into an overflow menu
- `material-variant` - Material style hint for Android, e.g. `outlined` (optional). Set automatically when you use an `AndroidOutlined` enum case
- `label` - Text label. Used for accessibility and displayed in overflow menus (optional but recommended)
- `url` - A URL to navigate to when tapped (optional)
- `transition` <x-docs.version-badge since="4.8" /> - The transition used when tapping navigates to `url`, as a
[transition](../the-basics/routing#custom-transitions) value such as `slide_from_bottom` (optional, default: `none`).
Only applies to an action with a `url` and no `@tap`
- `destructive` - Render in the destructive (red) tint (optional, boolean)

<aside>
Expand All @@ -111,6 +114,23 @@ method on your screen instead of navigating, use `@tap="methodName"`.

</aside>

To present the destination like a sheet, give the action a `transition`:

@verbatim
```blade static
<native:top-bar-action
id="settings"
icon="settings"
label="Settings"
url="/settings"
transition="slide_from_bottom"
/>
```
@endverbatim

The settings screen can then dismiss itself with `$this->back(Transition::SlideToBottom)`. See
[Custom transitions](../the-basics/routing#custom-transitions).

### Platform icons

`<native:top-bar-action>` resolves icons through the same contract as [`<native:icon>`](icon#typed-icon-enums): a
Expand Down Expand Up @@ -205,9 +225,12 @@ class PhotoViewer extends NativeComponent
This is equivalent to returning `NavBarOptions::make()->hidden()` from `navigationOptions()`; if both are set, the
explicit builder wins. It's the top-bar parallel to the tab bar's [`hidden()`](bottom-nav#per-screen-tab-bar).

Navigation keeps working while the bar is hidden — pushes, pops, and `@navigate` all behave normally. With the bar
hidden the screen is full-bleed: on Android the content extends up to the very top edge, under the transparent
status bar; use the safe-area utilities on elements that should stay clear of the clock and status icons. On iOS
With native chrome, the bar isn't removed: its config stays on the screen and is marked hidden, so the
`NavigationStack` stays in place. Navigation keeps working while the bar is hidden — pushes, pops, and `@navigate` all
behave normally.

With the bar hidden the screen is full-bleed: on Android the content extends up to the very top edge, under the
transparent status bar; use the safe-area utilities on elements that should stay clear of the clock and status icons. On iOS
the edge-swipe-back gesture is tied to the visible bar, so a pushed screen that hides it should render its own back
control (for example a floating button that uses `@navigate` to return to the parent screen).

Expand Down
35 changes: 35 additions & 0 deletions resources/views/docs/mobile/4/getting-started/upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,41 @@ title: Upgrade Guide
order: 3
---

## Upgrading To 4.8 From 4.7

After `composer update nativephp/mobile`, run `php artisan native:install --force`. `composer update` doesn't touch
the native iOS and Android projects, so the navigation and transition changes in 4.8 only reach your app after a
reinstall.

### Layouts use native chrome by default

`NativeLayout::usesNativeChrome()` now returns `true`. If your layouts never overrode it, their nav and tab bars switch
from the custom-drawn `Column` chrome to the platform's native renderers (`NavigationStack` / `TabView` on iOS,
`NavHost` / `Scaffold` on Android). Inline `<native:top-bar>` and `<native:bottom-nav>` were already native, so this
brings layout-supplied bars in line with them.

Open a few existing screens on a device or simulator and check the chrome, especially screens that set
`$hidesNavBar` or `$hidesTabBar`. With native chrome those bars are marked hidden instead of being removed from the
tree.

To keep the old chrome for now, override the method and return `false`:

```php
public function usesNativeChrome(): bool
{
return false;
}
```

This opt-out is temporary and will be removed. See [Native chrome](../the-basics/layouts#native-chrome).

### Handlers that can't run now throw in debug

Handlers such as `@press="$showSheet = true"` now work (see [Handlers](../edge-components/introduction#handlers)).
Handlers that can't run are no longer dropped silently: with `app.debug` on they throw `InvalidHandlerException`, and
with it off they're logged. If an error screen names a handler after you upgrade, it's usually a typo in a method name
or an assignment whose value isn't a literal.

## Upgrading To 4.6 From 4.5

### Translated iOS permission strings need `supported_locales`
Expand Down
2 changes: 2 additions & 0 deletions resources/views/docs/mobile/4/the-basics/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,8 @@ These events of the package carry the marker:

| Event | Fires when | Arguments | More |
| --- | --- | --- | --- |
| `Native\Mobile\Events\App\AppBackgrounded` | The app goes to the background | None | [Lifecycle Hooks](../digging-deeper/lifecycle-hooks#the-app-going-to-the-background) |
| `Native\Mobile\Events\App\AppForegrounded` | The app comes back from the background | `secondsAway` | [Lifecycle Hooks](../digging-deeper/lifecycle-hooks#the-app-going-to-the-background) |
| `Native\Mobile\Events\System\AppearanceChanged` | The system switches between light and dark | `mode` | [Theming](../digging-deeper/theming#reacting-to-changes) |
| `Native\Mobile\Events\System\OrientationChanged` | The app window turns to portrait or landscape | `orientation` | [System](system#orientation) |
| `Native\Mobile\Events\Device\ThermalStateChanged` | The thermal state of the device changes | `state`, `previous` | [Device](device#events) |
Expand Down
43 changes: 43 additions & 0 deletions resources/views/docs/mobile/4/the-basics/layouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,42 @@ class SyncUpTabsLayout extends NativeLayout
The `$screen` parameter is the live `NativeComponent` instance for the current screen, so the layout can read
properties or methods on it (such as `$screen->navTitle()`) to customize the chrome per screen.

## Native chrome

<x-docs.version-badge changed="4.8" />

Layouts render their bars with the platform's own navigation chrome: a `NavigationStack` and `TabView` on iOS, a
`NavHost` and `Scaffold` on Android. You get edge-swipe back, predictive back, large titles, and Liquid Glass on
iOS 26+ without any extra setup. This is controlled by `usesNativeChrome()`, which returns `true` by default.

Before 4.8, the default was `false`, and a layout had to override `usesNativeChrome()` to get native chrome. A layout
that never overrode it now renders native bars.

To fall back to the custom-drawn `Column` chrome, override it and return `false`:

```php
use Native\Mobile\Edge\Layouts\NativeLayout;

class LegacyLayout extends NativeLayout
{
public function usesNativeChrome(): bool
{
return false;
}
}
```

> [!WARNING]
> The `Column` fallback is temporary. The switch will be removed, and native chrome will be the only option. Treat
> `return false` as a stopgap while you move a layout over.

### Hiding bars with native chrome

`$hidesNavBar` / `NavBarOptions::make()->hidden()` and `$hidesTabBar` / `TabBarOptions::make()->hidden()` still hide
a bar on one screen, but native chrome hides it differently. The `Column` path drops the bar from the tree. With
native chrome, the bar's config is kept and marked hidden, so the `NavigationStack` and `TabView` stay in place and
pushes, pops, and tab switches keep working across screens that hide a bar.

## Builder reference

The chrome is described entirely with these fluent builders — you never place a top bar or tab bar as an element
Expand Down Expand Up @@ -421,6 +457,13 @@ When a screen renders, the framework's `wrapWithChrome` flow:
2. Calls `$layout->navBar($screen)` and `$layout->tabBar($screen)`.
3. Merges in any `navigationOptions()` declared on the screen.
4. Merges in any imperative state set via `$this->setNavBar([...])` / `$this->setTabBar([...])`.

With [native chrome](#native-chrome) (the default), the screen content is then wrapped in a native root element that
carries the bar config as props, and the platform's navigation renderer draws the bars. You don't manage safe areas
for the bars yourself.

A layout that returns `false` from `usesNativeChrome()` takes the `Column` path instead:

5. Wraps the screen content in a `Column` filling the screen, with the bars stacked above and below the content.
6. Picks the right safe-area variant for the wrapper:
- **TabBar present** → wrapper uses `safeAreaTop()`; the bar handles its own bottom inset.
Expand Down
52 changes: 52 additions & 0 deletions resources/views/docs/mobile/4/the-basics/nested-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,16 @@ public function onTaskToggled(string $source, string $title, bool $done): void
}
```

A tag binding can also set one of the parent's properties instead of calling a method
<x-docs.version-badge since="4.8" />, using the same `$property = <literal>` syntax as any other
[handler](../edge-components/introduction#handlers). The emit's arguments are ignored:

@verbatim
```blade static
<native:task-editor :task-id="$task['id']" @saved="$editing = false" />
```
@endverbatim

**`#[On]` listeners** — a string-form `#[On('event-name')]` method fires on *any* ancestor, however deep the
emitter is. A grandchild's emit reaches the screen without the intermediate child forwarding anything:

Expand Down Expand Up @@ -244,6 +254,48 @@ parameters other names.

</aside>

## Skipping unchanged children with #[Memo]

<x-docs.version-badge since="4.8" />

A screen re-runs its Blade view on every frame: every tap, keystroke, async result, or native event. Its children
re-render with it, so a long list of extracted rows rebuilds every row even when only one changed. Add the `#[Memo]`
attribute to a child whose output depends only on its own props and state:

```php
namespace App\NativeComponents;

use Illuminate\View\View;
use Native\Mobile\Attributes\Memo;
use Native\Mobile\Edge\NativeComponent;

#[Memo]
class TaskRow extends NativeComponent
{
public array $task = [];

public function render(): View
{
return view('native.task-row');
}
}
```

When nothing the child renders from has changed, it reuses the elements it built last time and skips its Blade view.
It re-renders when any of these change:

- its public properties, both props and its own state
- the `@event` bindings on its mounting tag
- the public properties of any child nested inside it

A child's first render and streaming renders are never memoized.

> [!IMPORTANT]
> `#[Memo]` is opt-in because it only compares props, state, and bindings. If the child's `render()` or view also
> reads the time, the session, config, or the database, a memoized child keeps showing what it showed last. The same
> goes for a `#[Computed]` or `#[Poll]`-driven value that changes without a public property changing. Leave
> `#[Memo]` off those children.

## Lifecycle

- `mount()` runs when a child's key first appears in the parent's tree.
Expand Down
29 changes: 28 additions & 1 deletion resources/views/docs/mobile/4/the-basics/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ $this->back();
Pops the current screen off the stack and returns to whatever was beneath it. The default transition is a slide-out
to the right.

To dismiss a screen you presented from the bottom, pass a transition <x-docs.version-badge since="4.8" />:

```php
use Native\Mobile\Edge\Transition;

$this->back(Transition::SlideToBottom);
```

## Replacing the current screen

```php
Expand Down Expand Up @@ -80,11 +88,23 @@ native experience.

## Custom transitions

Chain `transition()` after a navigation method to override the default animation:
`navigate()`, `back()`, and `replace()` take an optional transition that overrides the default animation
<x-docs.version-badge since="4.8" />. It's the last argument, after the URI and data:

```php
use Native\Mobile\Edge\Transition;

$this->navigate('/settings', [], Transition::SlideFromBottom);
$this->replace('/login', [], Transition::None);
$this->back(Transition::SlideToBottom);
```

You can pass a `Transition` case or its string value, such as `'slide_from_bottom'`. Calls without a transition
behave as they did before.

You can also chain `transition()` after a navigation method:

```php
$this->navigate('/item/42')->transition(Transition::SlideFromBottom);
```

Expand All @@ -93,13 +113,20 @@ Available cases:
- `Transition::SlideFromRight` - default for `navigate()`
- `Transition::SlideFromLeft` - default for `back()`
- `Transition::SlideFromBottom` - modal-style presentation
- `Transition::SlideToBottom` <x-docs.version-badge since="4.8" /> - modal-style dismissal, the inverse of `SlideFromBottom`: the outgoing screen slides down off the bottom edge, revealing the screen beneath it. Pair the two for sheet-style present and dismiss
- `Transition::Fade` - default for `replace()`
- `Transition::FadeFromBottom` - subtle vertical fade
- `Transition::ScaleFromCenter` - zoom-in effect
- `Transition::ParallaxPush` - incoming screen slides in from the right while the outgoing screen drifts partially left underneath
- `Transition::Morph` <x-docs.version-badge since="4.7" /> - cross-fade while elements that share a `ref` move between the two screens. See [Morphing elements between screens](#morphing-elements-between-screens)
- `Transition::None` - swap with no animation

<aside>

In 4.8, `SlideToBottom` animates on iOS only. On Android it currently falls back to a cross-fade.

</aside>

## Navigating from Blade

The `@navigate` directive triggers navigation directly from your view, with no component method required. It works on
Expand Down
Loading