Widget 代码实战 1:冒险开始
Widgets Code-along, part 1: The adventure begins
2020年6月25日
一句话判断
这是 WidgetKit 系列实战的第一集,从零开始创建一个角色冒险 Widget——如果你偏好”跟着做”而不是”看文档”,这个三集系列是最好的 WidgetKit 入门教程。
这场 Session 讲了什么
Session 以一个角色冒险游戏(Character Tracker)为案例,从创建 Widget Extension 开始,逐步构建一个展示角色状态信息的 Widget。
第一集的内容包括:在 Xcode 中添加 Widget Extension target、理解 Widget 的项目结构和文件组成、创建最简单的 StaticConfiguration Widget、实现 TimelineProvider 的三个必要方法(placeholder、getSnapshot、getTimeline),以及编写第一个 SwiftUI Widget 视图。
整个过程中穿插了 Widget 的设计思考:角色的生命值和等级是核心信息,适合放在 small Widget;而角色的装备列表和任务进度则适合 medium 或 large Widget。这种”从数据出发决定展示”的思路值得学习。
值得深挖的点
Widget Extension 的独立进程模型。 Widget 运行在独立于主 App 的进程中。这意味着它不能直接访问主 App 的内存空间或单例对象。数据共享必须通过 App Group 的共享 UserDefaults 或共享文件容器。这是初学者最容易踩的坑——Widget 代码里直接调用主 App 的 DataManager 会得到空数据。
TimelineEntry 的设计是 Widget 架构的起点。 Entry 定义了 Widget 在某个时间点需要展示的所有数据。设计 Entry 时要考虑:哪些数据是时间相关的(如倒计时、进度条)、哪些数据在一段时间内不会变化(如角色名字、头像)。好的 Entry 设计能让 Timeline 的生成逻辑更清晰。
代码片段
import WidgetKit
import SwiftUI
// 定义角色数据模型
struct Character: Codable {
let name: String
let level: Int
let health: Int
let maxHealth: Int
}
// Timeline Entry
struct CharacterEntry: TimelineEntry {
let date: Date
let character: Character
}
// Timeline Provider
struct CharacterProvider: TimelineProvider {
func placeholder(in context: Context) -> CharacterEntry {
CharacterEntry(
date: Date(),
character: Character(name: "勇士", level: 5, health: 80, maxHealth: 100)
)
}
func getSnapshot(in context: Context, completion: @escaping (CharacterEntry) -> Void) {
let character = loadCharacterFromAppGroup()
completion(CharacterEntry(date: Date(), character: character))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<CharacterEntry>) -> Void) {
let character = loadCharacterFromAppGroup()
let entry = CharacterEntry(date: Date(), character: character)
// 简单的单条时间线,每小时刷新一次
let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: Date())!
let timeline = Timeline(
entries: [entry],
policy: .after(nextUpdate)
)
completion(timeline)
}
// 通过 App Group 共享数据
private func loadCharacterFromAppGroup() -> Character {
let defaults = UserDefaults(suiteName: "group.com.example.adventure")
guard let data = defaults?.data(forKey: "character"),
let character = try? JSONDecoder().decode(Character.self, from: data) else {
return Character(name: "未知", level: 1, health: 100, maxHealth: 100)
}
return character
}
}
// Widget 视图
struct CharacterWidgetEntryView: View {
var entry: CharacterEntry
var body: some View {
VStack(spacing: 8) {
// 角色头像和等级
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 40, height: 40)
.foregroundColor(.purple)
Text(entry.character.name)
.font(.headline)
Text("Lv.\(entry.character.level)")
.font(.subheadline)
.foregroundColor(.secondary)
// 生命值进度条
ProgressView(
value: Double(entry.character.health),
total: Double(entry.character.maxHealth)
)
.tint(entry.character.health > 30 ? .green : .red)
Text("\(entry.character.health)/\(entry.character.maxHealth) HP")
.font(.caption2)
}
.padding()
}
}
最佳实践
- 第一步就配置好 App Group。 Widget 和主 App 的数据共享依赖 App Group,没有它 Widget 拿不到任何数据。
- placeholder 数据要真实且有意义。 不要用空白或”无数据”,用代表性的数据展示 Widget 的最终效果。
- getSnapshot 返回最近的真实数据。 Widget Gallery 中的预览直接影响用户是否决定添加你的 Widget。
- 先跑通最简单的单条 Timeline,再考虑复杂的多条时间线策略。
还有什么值得关注
- Xcode 的 Widget Extension 模板会自动生成基本结构,包括 Info.plist 配置
- 系列的第二集会介绍多尺寸适配和 Timeline 策略
- 第三集会深入讲解用户可配置的 Intent Widget
- Widget 的调试可以在 Xcode 中选择 Widget scheme 运行