返回 Skill 列表
extension
分类: 开发与工程无需 API Key

rn-ios-extensions

使用React Native构建原生iOS扩展-Apple Watch应用程序、Dynamic Island、Live Activities、小部件和分享意图。在使用expo-apple-targets、Voltra、expo-share-intent或不离开RN的情况下构建原生iOS功能时使用。

person作者: jakexiaohubgithub

React Native iOS Extensions

Build native iOS experiences from your React Native codebase using three complementary libraries.

Quick Reference

| Feature | Library | Notes | |---------|---------|-------| | Apple Watch apps | expo-apple-targets | Full SwiftUI, WatchConnectivity | | App Clips | expo-apple-targets | Can include React Native | | Safari Extensions | expo-apple-targets | JavaScript injection support | | Widgets (SwiftUI) | expo-apple-targets | Native SwiftUI required | | Widgets (JSX) | Voltra | React components, no Swift | | Dynamic Island | Voltra | JSX-based layouts | | Live Activities | Voltra | Lock screen, push updates | | Share Sheet | expo-share-intent | iOS + Android support |


expo-apple-targets

Generate native Apple targets (widgets, watch apps, clips) outside /ios while keeping Continuous Native Generation benefits.

Requirements

  • CocoaPods 1.16.2+ (Ruby 3.2.0)
  • Xcode 16+ (macOS 15 Sequoia)
  • Expo SDK 52+

Setup

# Generate a target (e.g., widget, watch, clip)
npx create-target widget

# Set Apple Team ID in app.json
# Run prebuild
npx expo prebuild -p ios --clean

# Open Xcode and develop in expo:targets/<target>
xed ios

Target Configuration

Each target needs targets/<name>/expo-target.config.js:

/** @type {import('@bacons/apple-targets/app.plugin').Config} */
module.exports = {
  type: "widget",
  name: "My Widget",
  icon: "../assets/icon.png",
  deploymentTarget: "17.0",
  colors: {
    $accent: { color: "red", darkColor: "blue" },
    $widgetBackground: "#1a1a1a",
  },
  entitlements: {
    "com.apple.security.application-groups": ["group.com.myapp.data"],
  },
};

Sharing Data Between App and Target

  1. Configure App Groups in app.json:
{
  "expo": {
    "ios": {
      "entitlements": {
        "com.apple.security.application-groups": ["group.com.myapp.data"]
      }
    }
  }
}
  1. Set data from React Native:
import { ExtensionStorage } from "@bacons/apple-targets";

const storage = new ExtensionStorage("group.com.myapp.data");
storage.set("myKey", { value: 42 });
ExtensionStorage.reloadWidget();
  1. Read in Swift:
let defaults = UserDefaults(suiteName: "group.com.myapp.data")
let value = defaults?.string(forKey: "myKey")

For full target types and configuration, see references/expo-apple-targets.md.


Voltra

Build Live Activities, Dynamic Island layouts, and widgets using React components. No Swift required.

Installation

npm install voltra

Add to app.json:

{
  "plugins": ["voltra"]
}

Live Activity

import { startLiveActivity } from 'voltra/client';
import { Voltra } from 'voltra';

const activity = (
  <Voltra.VStack style={{ padding: 16, backgroundColor: '#101828' }}>
    <Voltra.Symbol name="timer" tintColor="#38BDF8" />
    <Voltra.Text style={{ color: '#F8FAFC', fontSize: 18 }}>
      Recording: 02:34
    </Voltra.Text>
  </Voltra.VStack>
);

await startLiveActivity({ lockScreen: activity });

Hook API (with hot reload)

import { useLiveActivity } from 'voltra/client';

function RecordingActivity() {
  const { start, update, end } = useLiveActivity();
  
  const handleStart = () => {
    start({
      lockScreen: <TimerUI elapsed={0} />,
      dynamicIsland: {
        compact: <CompactUI />,
        expanded: <ExpandedUI />,
      },
    });
  };
}

Widget

import { Voltra } from 'voltra';

export function MyWidget() {
  return (
    <Voltra.VStack style={{ padding: 16 }}>
      <Voltra.Text style={{ fontSize: 24, fontWeight: '600' }}>
        Hello Widget
      </Voltra.Text>
    </Voltra.VStack>
  );
}

Push Updates (Server-Side)

import { renderLiveActivityToString } from 'voltra/server';
import { Voltra } from 'voltra';

const payload = renderLiveActivityToString({
  lockScreen: (
    <Voltra.VStack>
      <Voltra.Text>Updated from server!</Voltra.Text>
    </Voltra.VStack>
  ),
});

// Send payload via APNS

For components and API details, see references/voltra.md.


expo-share-intent

Handle shared content (URLs, text, images, files) from the iOS/Android share sheet.

Installation

npm install expo-share-intent patch-package

Add patch file from example/patches.

Configure app.json:

{
  "scheme": "myapp",
  "plugins": [
    ["expo-share-intent", {
      "iosActivationRules": {
        "NSExtensionActivationSupportsWebURLWithMaxCount": 1,
        "NSExtensionActivationSupportsImageWithMaxCount": 5,
        "NSExtensionActivationSupportsFileWithMaxCount": 1
      },
      "androidIntentFilters": ["text/*", "image/*"]
    }]
  ]
}

Usage

import { useShareIntent } from "expo-share-intent";

function App() {
  const { hasShareIntent, shareIntent, resetShareIntent } = useShareIntent();

  useEffect(() => {
    if (hasShareIntent) {
      console.log("Shared text:", shareIntent.text);
      console.log("Shared URL:", shareIntent.webUrl);
      console.log("Shared files:", shareIntent.files);
      resetShareIntent();
    }
  }, [hasShareIntent]);
}

Share Intent Data

| Property | Description | |----------|-------------| | shareIntent.text | Raw shared text or URL | | shareIntent.webUrl | Extracted URL from text | | shareIntent.files | Array of { path, mimeType, fileName, size } | | shareIntent.meta | Metadata including title and Open Graph tags |

For configuration options, see references/expo-share-intent.md.


Combined Patterns

Voltra Widget + expo-apple-targets for Advanced Features

Use Voltra for widget UI, expo-apple-targets for App Groups:

// React Native side
import { ExtensionStorage } from "@bacons/apple-targets";

const storage = new ExtensionStorage("group.com.myapp.data");
storage.set("widgetData", { count: 42, lastUpdated: Date.now() });
ExtensionStorage.reloadWidget();

Share Intent to Live Activity

When content is shared, start a Live Activity:

import { useShareIntent } from "expo-share-intent";
import { startLiveActivity } from "voltra/client";

function App() {
  const { hasShareIntent, shareIntent } = useShareIntent();

  useEffect(() => {
    if (hasShareIntent && shareIntent.files?.length) {
      startLiveActivity({
        lockScreen: <ProcessingUI file={shareIntent.files[0]} />,
      });
    }
  }, [hasShareIntent]);
}

EAS Build Notes

  • All libraries work with EAS Build
  • expo-apple-targets handles code signing automatically
  • For expo-share-intent, ensure only one extension target during credentials setup
  • Don't commit /ios and /android folders; rebuild before EAS builds

Troubleshooting

| Issue | Solution | |-------|----------| | Widget not showing | Use iOS 18+, long-press app icon to add widget | | Share intent config sync failed | Add xcode+3.0.1.patch to patches folder | | Live Activity not updating | Check APNS configuration and push token | | Watch app not connecting | Verify WatchConnectivity session activation |