0%

在 React Native 项目中添加 iOS WidgetKit 小组件(iOS 17+)

在 React Native 项目中添加 iOS WidgetKit 小组件(iOS 17+)

✅ 支持 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 预览更稳定

步骤一:在 Xcode 中添加 Widget Extension

  1. cd ios && pod install 后,用 .xcworkspace 打开项目。
  2. 新建 Target → 选择 Widget Extension(SwiftUI)。
  3. 勾选 Include Configuration Intent(如需参数配置)。
  4. 命名为 MyAppWidget,生成后包含以下结构:
  • MyAppWidget.swift - 注册 Widget 条目
  • MyAppWidgetEntry.swift - TimelineEntry 模型
  • MyAppWidgetView.swift - SwiftUI UI 组件

步骤二:配置 App Group 和桥接 JS ⇄ Widget 数据

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
// WidgetBridge.swift
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
// WidgetBridge.ts
import { NativeModules } from 'react-native';
export const updateWidget = (count: number) =>
NativeModules.WidgetBridge.update({ count });

步骤三:编写 Widget UI 和时间线逻辑

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>
);
}

步骤五:打包 & 注意事项

5.1 Podfile 中添加 Widget 支持

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 小组件,也都可以类似拓展。祝接入顺利!