Meet WidgetKit
Swift & UI 入门 28m

初识 WidgetKit

Meet WidgetKit

2020年6月22日

在 Apple 官方观看视频

一句话判断

iOS 14 的 Widget 是今年最大的 UI 变革——WidgetKit 用 SwiftUI 声明式 UI + Timeline Provider 的架构,彻底取代了_today extension_时代的老旧方案,每个 iOS 开发者都应该了解。

这场 Session 讲了什么

WidgetKit 是 iOS 14 中全新的框架,用于在主屏幕和”今天”视图中展示 App 的信息卡片。与旧版 Today Extension 的最大区别是:Widget 不是迷你 App,而是纯展示组件——用户不能直接在 Widget 上交互,点击 Widget 会跳转到主 App。

架构上,Widget 由三个核心部分组成:TimelineProvider(提供 Widget 显示的数据和时间线)、Widget(SwiftUI 视图,定义 Widget 的外观)、以及 WidgetBundle(如果 App 有多个 Widget,用 Bundle 组织它们)。

Timeline 是 Widget 的核心概念。你可以提前提供一组带时间戳的数据(Timeline Entry),系统会在对应时间自动刷新 Widget。比如天气 App 可以提供未来 24 小时每小时一条的天气数据,系统会在每个整点自动更新显示,无需 App 在后台运行。

值得深挖的点

Timeline 策略决定了 Widget 的”新鲜度”。 系统并不会严格按你提供的时间线刷新——它有一个全局的刷新预算,会根据用户的使用习惯、电池状态等因素调整。你需要告诉系统下一次刷新的最早时间(nextUpdateDate),但实际刷新可能晚于这个时间。

Widget 的三种尺寸。 系统提供 small、medium、large 三种尺寸。你的 SwiftUI 视图需要通过 @Environment(\.widgetFamily) 来适配不同尺寸。不是所有内容都适合所有尺寸——small 只能显示一两个关键数据,large 才能展示列表。

代码片段

import WidgetKit
import SwiftUI

// 1. 定义 Timeline Entry——每个时间点的数据
struct SimpleEntry: TimelineEntry {
    let date: Date          // 这条数据应该在什么时间显示
    let configuration: ConfigurationIntent
    let temperature: Double
    let condition: String
}

// 2. Timeline Provider——系统调用这个来获取数据
struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        // 占位数据,在 Widget 首次加载或加载中时显示
        SimpleEntry(date: Date(), configuration: .init(), temperature: 22, condition: "晴")
    }
    
    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
        // 在 Widget Gallery 中展示的预览数据
        completion(SimpleEntry(date: Date(), configuration: .init(), temperature: 22, condition: "晴"))
    }
    
    func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
        // 提供完整的时间线数据
        let entries = (0..<24).map { hour -> SimpleEntry in
            let entryDate = Calendar.current.date(byAdding: .hour, value: hour, to: Date())!
            return SimpleEntry(
                date: entryDate,
                configuration: .init(),
                temperature: Double.random(in: 15...30),
                condition: "多云"
            )
        }
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}
// 3. Widget 的 SwiftUI 视图
struct WeatherWidgetEntryView: View {
    var entry: Provider.Entry
    @Environment(\.widgetFamily) var family
    
    var body: some View {
        switch family {
        case .systemSmall:
            VStack {
                Text("\(Int(entry.temperature))°")
                    .font(.largeTitle)
                    .bold()
                Text(entry.condition)
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
        case .systemMedium:
            HStack {
                VStack(alignment: .leading) {
                    Text("当前天气")
                        .font(.caption)
                    Text("\(Int(entry.temperature))°")
                        .font(.title)
                        .bold()
                }
                Spacer()
                Text(entry.condition)
                    .font(.title3)
            }
            .padding()
        default:
            // Large 尺寸可以显示更多内容
            VStack(alignment: .leading) {
                Text("天气预报")
                    .font(.headline)
                Divider()
                Text("\(Int(entry.temperature))° - \(entry.condition)")
            }
            .padding()
        }
    }
}
// 4. 组装 Widget
@main
struct WeatherWidgetBundle: WidgetBundle {
    var body: some Widget {
        WeatherWidget()
    }
}

struct WeatherWidget: Widget {
    let kind: String = "WeatherWidget"
    
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            WeatherWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("天气")
        .description("显示当前和未来天气")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

最佳实践

  • 提供丰富的 Timeline 数据。 不要只给当前时间的数据,预生成未来几个小时甚至一天的数据,让系统在合适的时机自动刷新。
  • 为 Widget Gallery 提供有意义的快照数据。 getSnapshot 的数据会直接展示在用户选择 Widget 的画廊中,用真实数据比占位数据更能吸引用户添加。
  • small 尺寸只展示最关键的信息。 一两个数字、一个图标加状态文字就够了。不要试图在小 Widget 里塞太多内容。
  • 使用 .widgetURL.link 处理点击跳转。 Widget 点击后通过 Deep Link 跳转到 App 的对应页面,让用户快速获取详细信息。

还有什么值得关注

  • Widget 支持 Intent Configuration,用户可以在 Widget 上长按配置参数(如选择城市)
  • Timeline Policy 有三种:.atEnd、.after(date)、.never,控制下次刷新时机
  • 系统限制每个 App 的 Widget 每天刷新次数,不要浪费刷新预算
  • Widget 跑在独立的进程中,与主 App 的数据共享需要通过 App Group
WWDC 2020