Skip to content
Draft
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
2 changes: 2 additions & 0 deletions resources/views/docs/mobile/4/getting-started/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ For changes prior to v4, see the [v3 documentation](/docs/mobile/3/getting-start
### For Plugin Developers

- **No breaking changes** to the plugin architecture — add the `^4.0` constraint to your plugin's `nativephp/mobile` dependency and you're done
- **[iOS app-extension targets](../plugins/ios-app-extensions)** — plugins can declare WidgetKit extension targets with isolated sources, derived bundle identifiers, App Groups, and extension signing
- **Safer entitlement merging** — existing host scalar values now win, list values are unioned without duplicates, and nested dictionaries merge recursively
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,12 @@ Values can be:

Entitlements are written to `NativePHP.entitlements`. If the file doesn't exist, it's created automatically.

When entitlements already exist, host scalar values win, lists are unioned without duplicates, and nested dictionaries
merge recursively. This prevents a plugin from silently replacing an application's existing signing configuration.

Plugins that ship WidgetKit code should also read [iOS App Extensions](ios-app-extensions) for App Group, extension
entitlement, and provisioning-profile requirements.

<aside>

Many entitlements require corresponding capabilities enabled in your Apple Developer account and Xcode project settings.
Expand Down
1 change: 1 addition & 0 deletions resources/views/docs/mobile/4/plugins/creating-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ The manifest declares native-specific configuration for your plugin. Package met
| `ios.dependencies` | No | Swift packages and CocoaPods |
| `ios.background_modes` | No | UIBackgroundModes values |
| `ios.entitlements` | No | App entitlements |
| `ios.extension_targets` | No | Isolated [iOS app-extension targets](ios-app-extensions) |
| `ios.capabilities` | No | iOS capabilities for Xcode project |
| `ios.min_version` | No | Minimum iOS version required |
| `ios.init_function` | No | Native function to call during app initialization |
Expand Down
189 changes: 189 additions & 0 deletions resources/views/docs/mobile/4/plugins/ios-app-extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
---
title: iOS App Extensions
order: 725
---

NativePHP plugins can ship isolated iOS app-extension targets alongside the host application. The initial supported
extension type is a WidgetKit widget extension.

App extensions have their own bundle identifier, sources, `Info.plist`, entitlements, build settings, and signing
profile. NativePHP generates the target and embeds its `.appex` product into both the device and simulator hosts.

## Plugin structure

Keep host bridge code and extension code in separate directories. Extension sources are compiled only into the extension
target and are not copied into the host application.

```text
resources/
└── ios/
├── Sources/
│ └── WidgetBridge.swift
└── extension/
└── MyWidgets.swift
```

The extension source must contain a valid WidgetKit entry point, such as an `@main` `Widget` or `WidgetBundle`.

## Declaring a widget extension

Add an `extension_targets` list to the plugin's `ios` configuration:

```json
{
"namespace": "MyWidgets",
"ios": {
"min_version": "17.0",
"info_plist": {
"NativePHPWidgetAppGroup": "group.${APP_ID}.widgets"
},
"entitlements": {
"com.apple.security.application-groups": [
"group.${APP_ID}.widgets"
]
},
"extension_targets": [
{
"name": "MyWidgetsExtension",
"type": "widget-extension",
"bundle_id_suffix": "widgets",
"deployment_target": "17.0",
"sources_dir": "extension",
"info_plist": {
"CFBundleDisplayName": "My Widgets",
"NativePHPWidgetAppGroup": "group.${APP_ID}.widgets"
}
}
]
}
}
```

This example generates the extension bundle identifier `{APP_ID}.widgets` and the shared App Group
`group.{APP_ID}.widgets`.

### Target fields

| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Unique Xcode target name using letters, numbers, and underscores |
| `type` | Yes | Must currently be `widget-extension` |
| `bundle_id_suffix` | Yes | Component appended to the host application identifier |
| `sources_dir` | Yes | Directory below `resources/ios` containing extension-only sources |
| `deployment_target` | No | Extension iOS deployment target; defaults to `ios.min_version` or `17.0` |
| `info_plist` | No | Additional extension `Info.plist` values |

NativePHP owns the bundle identifier, executable, version, package type, and other compiler-generated plist keys. A
manifest that attempts to override those keys fails validation.

## Manifest environment values

`${APP_ID}` is always available to extension plist and entitlement values. Every other `${ENV_VAR}` placeholder must be
declared in the plugin's top-level `secrets` allowlist:

```json
{
"secrets": {
"PUBLIC_WIDGET_API_KEY": {
"description": "Public API key embedded in the widget extension",
"required": true
}
},
"ios": {
"extension_targets": [
{
"name": "MyWidgetsExtension",
"type": "widget-extension",
"bundle_id_suffix": "widgets",
"sources_dir": "extension",
"info_plist": {
"PublicWidgetAPIKey": "${PUBLIC_WIDGET_API_KEY}"
}
}
]
}
}
```

An undeclared placeholder fails the build instead of reading an arbitrary build-machine environment value. Values placed
in a plist or entitlement ship inside the built product, so never use this mechanism for signing credentials, private API
tokens, or server-side secrets.

## Sharing data with the host app

Widget extensions run outside the host application process. Use an App Group to share data between the NativePHP host
and the extension:

1. Declare the same `com.apple.security.application-groups` value in `ios.entitlements`.
2. Pass the generated group name to both the host and extension, commonly through `Info.plist`.
3. Read and write shared values with `UserDefaults(suiteName:)` or an App Group container.
4. Ask WidgetKit to reload the relevant timeline after the host writes new data.

Plugin entitlements merge into the existing host entitlement file. Existing host scalar values win, list values are
unioned without duplicates, and nested dictionaries merge recursively. Only the App Group entitlement is copied from the
plugin into the generated widget extension.

<aside>

App Groups must also exist for the host and extension identifiers in your Apple Developer account and provisioning
profiles.

</aside>

## Background widget updates

WidgetKit does not keep the NativePHP application or PHP runtime continuously running after the app closes. To update a
widget in the background, its `TimelineProvider` should return dated future entries. WidgetKit can display those entries
while the host process is terminated.

iOS controls refresh budgets and may delay requested reloads. Timeline dates are display guidance rather than real-time
scheduling guarantees. Fetch or prepare enough data for useful future entries and use an appropriate timeline reload
policy.

See Apple's documentation on
[keeping a widget up to date](https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date).

## Signing and packaging

Development builds can use automatic signing. Manual distribution signing requires a provisioning profile for every
generated extension bundle identifier in addition to the host profile.

Set `IOS_EXTENSION_PROVISIONING_PROFILES` to a JSON object keyed by the complete extension bundle identifier. Values may
be profile paths or strict base64-encoded profile contents:

```shell
export IOS_EXTENSION_PROVISIONING_PROFILES='{
"com.example.myapp.widgets": "/secure/profiles/MyWidgets.mobileprovision"
}'
```

NativePHP validates that each profile's application identifier exactly matches its extension bundle identifier before
installing or using it.

## Building and testing

Validate the plugin, then rebuild the native project:

```shell
php artisan native:plugin:validate
php artisan native:run
```

For a WidgetKit plugin, verify all of the following on a simulator or device:

1. The widget appears in the iOS widget gallery.
2. Each supported family renders real and placeholder data.
3. Host writes reach the extension through the App Group.
4. Widget taps open the expected deep link.
5. A timeline advances after the host application is terminated.
6. A release archive contains the extension under `PlugIns/*.appex`.

Opening the generated project in Xcode is supported. NativePHP uses deterministic object identifiers to recover and
recreate managed extension objects if Xcode rewrites the project and removes NativePHP's marker comments.

## Current limitations

- Only `widget-extension` targets are supported.
- This configuration is iOS-specific; Android widget components continue to use Android manifest and asset declarations.
- NativePHP manages the generated target. Add persistent extension configuration to `nativephp.json` and source files,
rather than editing generated Xcode objects by hand.
9 changes: 9 additions & 0 deletions tests/Feature/DocsPrereleaseVersionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,15 @@ public function test_the_basics_routing_and_layouts_pages(): void
$this->get('/docs/mobile/4/the-basics/layouts')->assertOk();
}

public function test_ios_app_extension_documentation_renders(): void
{
$this->get('/docs/mobile/4/plugins/ios-app-extensions')
->assertOk()
->assertSee('iOS App Extensions')
->assertSee('extension_targets')
->assertSee('IOS_EXTENSION_PROVISIONING_PROFILES');
}

public function test_moved_core_builtin_docs_redirect_from_plugins_core_to_the_basics(): void
{
$this->get('/docs/mobile/4/plugins/core/device')
Expand Down