Build SwiftUI views for widgets
Swift & UI 进阶 22m

为 Widget 构建 SwiftUI 视图

Build SwiftUI views for widgets

2020年6月24日

在 Apple 官方观看视频

一句话判断

Widget 的 SwiftUI 视图不是普通 App 视图的缩小版——系统对其有很多限制(不支持滚动、不支持动画、不能用某些 View Modifier),这场 Session 教你如何在这个约束条件下做出好看的 Widget。

这场 Session 讲了什么

Widget 的 UI 完全用 SwiftUI 构建,但不能使用所有 SwiftUI 特性。Session 详细列出了哪些可用、哪些不可用。不可用的包括:滚动视图、导航链接、动画、按钮交互、文本输入等。可用的核心是静态布局:VStack、HStack、ZStack、Image、Text、Shape 以及各种 View Modifier。

Session 重点讲解了 Widget 视图的设计原则。Widget 在不同尺寸下(small、medium、large)应该展示不同层级的信息。small 只展示一个核心数据点,medium 可以展示标题+数据+趋势,large 可以展示多条数据或详细内容。

另一个重要话题是 Widget 的渲染机制。系统会将 Widget 的 SwiftUI 视图渲染为快照(类似截图),显示在主屏幕上。这意味着你的 Widget 视图代码不会持续运行——它只在系统刷新 Widget 时执行一次。

值得深挖的点

Widget 视图的生命周期是”快照式”的。 与 App 内的 SwiftUI 视图不同,Widget 视图不会持续存在。系统在刷新时执行你的视图代码,将结果渲染为位图,然后显示在主屏幕上。直到下次刷新前,视图代码都不会再执行。这解释了为什么动画和交互在 Widget 中不可用。

containerBackground 和 Widget 的视觉一致性。 Widget 需要与系统主屏幕风格保持一致。Session 建议使用系统提供的容器背景,让 Widget 的外观随系统主题变化。自定义背景时要确保在浅色和深色模式下都好看。

代码片段

import SwiftUI
import WidgetKit

// Small Widget——只展示最核心的信息
struct SmallWidgetView: View {
    var entry: StepEntry
    
    var body: some View {
        VStack(spacing: 4) {
            Image(systemName: "flame.fill")
                .font(.title2)
                .foregroundColor(.orange)
            Text("\(entry.steps)")
                .font(.system(.title, design: .rounded))
                .bold()
            Text("步")
                .font(.caption2)
                .foregroundColor(.secondary)
        }
        .containerBackground(for: .widget) {
            // 系统提供的容器背景
            Color.clear
        }
    }
}
// Medium Widget——标题 + 数据 + 趋势
struct MediumWidgetView: View {
    var entry: StepEntry
    
    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 8) {
                Text("今日步数")
                    .font(.caption)
                    .foregroundColor(.secondary)
                
                Text("\(entry.steps)")
                    .font(.system(.title, design: .rounded))
                    .bold()
                
                // 进度条
                ProgressView(value: Double(entry.steps), total: 10000)
                    .tint(entry.steps >= 10000 ? .green : .blue)
            }
            
            Spacer()
            
            // 目标完成状态
            if entry.steps >= 10000 {
                Image(systemName: "checkmark.circle.fill")
                    .font(.largeTitle)
                    .foregroundColor(.green)
            }
        }
        .padding()
        .containerBackground(for: .widget) {
            Color.clear
        }
    }
}
// Large Widget——多组数据展示
struct LargeWidgetView: View {
    var entry: WeeklyEntry
    
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("本周步数")
                .font(.headline)
            
            ForEach(entry.dailySteps.prefix(7)) { day in
                HStack {
                    Text(day.weekday)
                        .font(.caption)
                        .frame(width: 30, alignment: .leading)
                    
                    // 简单的条形图
                    GeometryReader { geo in
                        RoundedRectangle(cornerRadius: 2)
                            .fill(day.steps >= 10000 ? Color.green : Color.blue)
                            .frame(width: geo.size.width * min(Double(day.steps) / 10000, 1.0))
                    }
                    .frame(height: 8)
                    
                    Text("\(day.steps)")
                        .font(.caption2)
                        .foregroundColor(.secondary)
                }
            }
        }
        .padding()
        .containerBackground(for: .widget) {
            Color.clear
        }
    }
}

最佳实践

  • 为每种尺寸单独设计视图。 不要试图用一个视图适配所有尺寸,small 和 large 的信息密度完全不同。
  • 使用 containerBackground 保持视觉一致性。 不要硬编码背景色,让系统决定 Widget 的容器样式。
  • 测试深色模式。 Widget 在深色主屏幕上的可读性与浅色模式下不同,确保文字和图标的对比度足够。
  • 控制视图层级深度。 Widget 渲染有时间和内存限制,过于复杂的视图层级可能导致渲染失败。

还有什么值得关注

  • Widget 视图中可以使用 Link 来创建可点击区域,跳转到 App 的不同页面
  • IntentConfiguration 可以让用户自定义 Widget 内容(如选择追踪的城市)
  • 系统会在 Widget Gallery 中使用 getSnapshot 的数据渲染预览
  • Widget 的视图代码应该尽量轻量,避免复杂的计算
WWDC 2020