Widgets Code-along, part 1: The adventure begins
Swift & UI 入门 36m

Widget 代码实战 1:冒险开始

Widgets Code-along, part 1: The adventure begins

2020年6月25日

在 Apple 官方观看视频

一句话判断

这是 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 运行
WWDC 2020