✅ 支持 React Native 0.74+、Xcode 15.3+、iOS 17 SDK
✅ 可读写共享数据,通过 App Group 和原生桥接与 JS 通信
✅ 支持静态展示和互动式(AppIntents)小组件
环境准备
组件 |
版本要求 |
说明 |
Xcode |
≥ 15.3 |
含 iOS 17 SDK,支持 AppIntents |
React Native |
≥ 0.74 |
默认启用 Hermes、支持架构升级 |
Expo(可选) |
≥ SDK 50 |
自动管理原生配置 |
macOS |
≥ Sonoma |
Widget 预览更稳定 |
cd ios && pod install
后,用 .xcworkspace
打开项目。
- 新建 Target → 选择 Widget Extension(SwiftUI)。
- 勾选 Include Configuration Intent(如需参数配置)。
- 命名为 MyAppWidget,生成后包含以下结构:
MyAppWidget.swift
- 注册 Widget 条目
MyAppWidgetEntry.swift
- TimelineEntry 模型
MyAppWidgetView.swift
- SwiftUI UI 组件
2.1 开启 App Group
- 在主 App Target → Signing & Capabilities 添加 App Group。
- 创建如
group.com.example.myapp.shared
,Widget Target 也加上它。
2.2 创建 Swift 原生桥接
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import Foundation import WidgetKit
@objc(WidgetBridge) class WidgetBridge: NSObject { @objc static func requiresMainQueueSetup() -> Bool { false }
@objc func update(_ dict: NSDictionary) { let defaults = UserDefaults(suiteName: "group.com.example.myapp.shared")! defaults.set(dict["count"], forKey: "count") WidgetCenter.shared.reloadAllTimelines() } }
|
2.3 暴露给 React Native
在 .m
文件或 Bridging Header 中注册为 Native Module:
1 2 3 4
| import { NativeModules } from 'react-native'; export const updateWidget = (count: number) => NativeModules.WidgetBridge.update({ count });
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| struct SimpleEntry: TimelineEntry { let date: Date let count: Int }
struct Provider: TimelineProvider { func placeholder(in ctx: Context) -> SimpleEntry { .init(date: .now, count: 0) } func getSnapshot(in ctx: Context, completion: @escaping (SimpleEntry) -> Void) { completion(entry()) } func getTimeline(in ctx: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) { completion(Timeline(entries: [entry()], policy: .after(.now + 1800))) }
private func entry() -> SimpleEntry { let defaults = UserDefaults(suiteName: "group.com.example.myapp.shared")! return .init(date: .now, count: defaults.integer(forKey: "count")) } }
struct MyAppWidgetEntryView: View { var entry: Provider.Entry var body: some View { VStack { Text("📊 次数") Text("\(entry.count)") .font(.largeTitle) .bold() } .widgetURL(URL(string: "myapp://counter")) } }
@main struct MyAppWidget: Widget { var body: some WidgetConfiguration { StaticConfiguration(kind: "MyAppWidget", provider: Provider()) { entry in MyAppWidgetEntryView(entry: entry) } .supportedFamilies([.systemSmall, .systemMedium]) } }
|
步骤四:React Native 端调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import React, { useState } from 'react'; import { View, Text, Button } from 'react-native'; import { updateWidget } from './WidgetBridge';
export default function CounterScreen() { const [count, setCount] = useState(0);
return ( <View> <Text>{count}</Text> <Button title="增加" onPress={() => { const next = count + 1; setCount(next); updateWidget(next); }} /> </View> ); }
|
步骤五:打包 & 注意事项
1 2 3 4
| target 'MyAppWidgetExtension' do inherit! :search_paths use_frameworks! end
|
5.2 Xcode Archive 注意事项
- 勾选 Widget target 的 Build。
- 如使用 Expo,需先
expo prebuild
并用 expo config plugins
添加 app.extensions
。
常见问题与解决
问题 |
可能原因 |
解决方法 |
Widget 无法读取数据 |
UserDefaults 组名错误 |
确认 App Group 一致 |
Widget 显示空白 |
getTimeline 没有返回 entry |
检查 timeline 返回值 |
React Native 调用无效 |
reloadAllTimelines 只允许主 app 调用 |
不要在 extension 内调用 |
小结
UI 在 SwiftUI,数据在 App Group,触发在 WidgetCenter。把 React Native 仅当“数据生产者”,其它都按原生 iOS WidgetKit 流程走,既能保证性能,又不会和 RN 运行时耦合过深。
- 第 1 次集成建议仅做 StaticConfiguration + 列表展示。
- 若要互动(按钮/滑块),升级到 AppIntents+Interactive Widgets。
- 若需要后台实时刷新,可结合 Push Widget Reload 或 BackgroundTasks。
按以上步骤,你就能在现有 RN 代码基础上,发布带 iOS 小组件 的版本,并且可以用熟悉的 JS 方法更新数据。后续如需深入 AppIntents、动态 Island(Live Activities)或 Android Glance 小组件,也都可以类似拓展。祝接入顺利!