[读取文件中]
以下是关于如何创建 iOS 锁屏小组件的详细介绍:
iOS 锁屏小组件(Lock Screen Widget)完整指南
一、基础概念
iOS 16 引入了锁屏小组件功能,基于 WidgetKit 框架实现,支持三种样式:
| 样式 | Family 枚举 | 描述 |
|---|---|---|
| 圆形 | .accessoryCircular |
圆形区域,适合图标+简短信息 |
| 矩形 | .accessoryRectangular |
横向矩形,适合多行文字 |
| 内联 | .accessoryInline |
单行文字,显示在时间旁边 |
二、前置条件
- Xcode 14+
- iOS 16.0+ SDK
- 项目中已有 Widget Extension Target
三、核心文件结构
LockScreenWidget/
└── LockScreenWidget.swift # 核心实现(Provider + Entry + View + Widget)
四、完整实现步骤
1. 定义数据模型 TimelineEntry
struct LockScreenEntry: TimelineEntry {
let date: Date
let songName: String
let artistName: String
let isPlaying: Bool
}
2. 实现 TimelineProvider
struct LockScreenProvider: TimelineProvider {
// 占位视图(首次加载时显示)
func placeholder(in context: Context) -> LockScreenEntry {
LockScreenEntry(date: Date(), songName: "Song Name", artistName: "Artist", isPlaying: false)
}
// 快照(小组件预览时使用)
func getSnapshot(in context: Context, completion: @escaping (LockScreenEntry) -> Void) {
let entry = LockScreenEntry(date: Date(), songName: "My Song", artistName: "My Artist", isPlaying: true)
completion(entry)
}
// 时间线(实际数据更新逻辑)
func getTimeline(in context: Context, completion: @escaping (Timeline<LockScreenEntry>) -> Void) {
// 从 App Group 共享数据或 API 获取数据
let entry = LockScreenEntry(date: Date(), songName: "My Song", artistName: "My Artist", isPlaying: true)
// 设置下次刷新时间
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: Date())!
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
3. 实现三种样式的 View
struct LockScreenWidgetEntryView: View {
var entry: LockScreenEntry
@Environment(\.widgetFamily) var family
var body: some View {
switch family {
case .accessoryCircular:
CircularView(entry: entry)
case .accessoryRectangular:
RectangularView(entry: entry)
case .accessoryInline:
InlineView(entry: entry)
default:
EmptyView()
}
}
}
// 圆形样式
struct CircularView: View {
var entry: LockScreenEntry
var body: some View {
ZStack {
AccessoryWidgetBackground() // 系统提供的背景
VStack(spacing: 2) {
Image(systemName: entry.isPlaying ? "play.circle.fill" : "pause.circle.fill")
.font(.system(size: 14))
Text("JOOX")
.font(.system(size: 8, weight: .bold))
}
}
}
}
// 矩形样式
struct RectangularView: View {
var entry: LockScreenEntry
var body: some View {
VStack(alignment: .leading, spacing: 2) {
Label(entry.songName, systemImage: entry.isPlaying ? "play.fill" : "pause.fill")
.font(.system(size: 12, weight: .semibold))
.lineLimit(1)
Text(entry.artistName)
.font(.system(size: 10))
.foregroundColor(.secondary)
.lineLimit(1)
}
}
}
// 内联样式
struct InlineView: View {
var entry: LockScreenEntry
var body: some View {
Label("\(entry.songName) - \(entry.artistName)",
systemImage: entry.isPlaying ? "play.fill" : "pause.fill")
}
}
4. 声明 Widget 并指定支持的 Family
struct LockScreenWidget: Widget {
let kind: String = "LockScreenWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: LockScreenProvider()) { entry in
LockScreenWidgetEntryView(entry: entry)
}
.configurationDisplayName("JOOX 锁屏")
.description("在锁屏上显示当前播放歌曲")
.supportedFamilies([
.accessoryCircular,
.accessoryRectangular,
.accessoryInline
])
}
}
5. 注册到 WidgetBundle
@main
struct MyWidgetBundle: WidgetBundle {
var body: some Widget {
// 其他小组件...
if #available(iOS 16.0, *) {
LockScreenWidget()
}
}
}
五、主 App 与 Widget 共享数据
锁屏小组件无法直接访问主 App 的数据,需要通过 App Group 共享:
// 主 App 写入数据
let defaults = UserDefaults(suiteName: "group.com.yourapp.widget")
defaults?.set("My Song", forKey: "currentSongName")
defaults?.set("My Artist", forKey: "currentArtistName")
// Widget 读取数据
let defaults = UserDefaults(suiteName: "group.com.yourapp.widget")
let songName = defaults?.string(forKey: "currentSongName") ?? ""
六、主动刷新 Widget
当主 App 中歌曲切换时,主动通知 Widget 刷新:
import WidgetKit
// 在主 App 中调用
WidgetCenter.shared.reloadTimelines(ofKind: "LockScreenWidget")
// 或刷新所有 Widget
WidgetCenter.shared.reloadAllTimelines()
七、锁屏小组件的设计注意事项
-
颜色渲染模式:锁屏小组件默认使用
.accented渲染模式,颜色会被系统统一处理,建议使用AccessoryWidgetBackground()作为背景 - 内容精简:锁屏空间有限,只展示最核心的信息
- 不支持动画:锁屏小组件不支持动画效果
-
点击跳转:可通过
Link或widgetURL设置点击后打开 App 的指定页面:.widgetURL(URL(string: "yourapp://nowplaying"))
八、完整流程图
graph TD
A[主 App 播放歌曲] --> B[写入 App Group UserDefaults]
B --> C[调用 WidgetCenter.reloadTimelines]
C --> D[WidgetKit 触发 getTimeline]
D --> E[Provider 读取 App Group 数据]
E --> F[生成 LockScreenEntry]
F --> G[渲染 LockScreenWidgetEntryView]
G --> H{widgetFamily}
H --> I[.accessoryCircular 圆形]
H --> J[.accessoryRectangular 矩形]
H --> K[.accessoryInline 内联]
九、常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 小组件不显示 | 未添加到 WidgetBundle | 确认 WidgetBundle 中已注册 |
| 数据不更新 | 未调用 reloadTimelines
|
在数据变化时主动调用刷新 |
| 颜色显示异常 | 锁屏颜色渲染限制 | 使用系统颜色或 .widgetAccentable()
|
| 编译报错 | iOS 版本不兼容 | 添加 @available(iOS 16.0, *) 检查 |