WWDC Quick Look 💓 By SwiftGGTeam
Principles of great widgets

Principles of great widgets

观看原视频

Highlight

iOS 15 为 WidgetKit 带来可配置性、隐私敏感度控制、以及针对不同环境和位置的适配能力,同时更新了设计原则确保小组件保持相关性和个性化。

核心内容

Widget 的核心价值是”一瞥即得”。用户不需要打开 App,就能在主屏幕、锁屏(iOS 16 起)或智能堆叠中获取关键信息。iOS 15 在这个基础上增加了三件事:让用户自定义 Widget 内容、保护敏感信息、以及让 Widget 适应不同的显示环境。

可配置性意味着用户可以决定 Widget 显示什么内容。股票 Widget 可以选择关注哪些股票,天气 Widget 可以选择关注哪些城市。iOS 15 引入了 IntentConfiguration,让用户通过长按 Widget 打开配置界面,选择自己关心的内容。

隐私敏感度控制解决的是”主屏幕上不应该显示私密信息”的问题。用户锁屏后,银行卡余额应该被遮挡。iOS 15 提供 .privacySensitive() 修饰符,系统会在锁屏或隐私模式下自动用占位符替代敏感内容。

环境适配指的是同一个 Widget 在不同位置可能有不同的呈现方式。主屏幕上的天气 Widget 可以显示详细温度,而在智能堆叠中可能只需要一个图标。iOS 15 为 Widget 提供了 widgetFamilyisPreview 等环境值,让开发者根据上下文调整布局。

详细内容

Xcode Previews 支持暗色模式

15:46)Widget 的 Xcode Previews 现在支持显式设置颜色方案,方便同时预览亮色和暗色效果。

struct MyWidget_Previews: PreviewProvider {
    static var previews: some View {
        MyWidgetEntryView(date: Date())
            .previewContext(WidgetPreviewContext(family: .systemSmall))
            .environment(\.colorScheme, .dark)
    }
}

关键点:

  • WidgetPreviewContext 指定 Widget 的尺寸系列
  • .environment(\.colorScheme, .dark) 切换到暗色模式预览
  • 可以创建多个 preview 实例同时展示不同尺寸和颜色方案

隐私敏感度控制

16:34)金融、健康等类别的 Widget 常常需要显示敏感数据。用户锁屏后,这些信息应该被遮挡。

struct MyWidgetEntryView: View {
    var body: some View {
        ZStack {
            Rectangle().fill(BackgroundStyle())
            VStack(alignment: .leading) {
                Text("Balance")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .foregroundColor(Color.blue)
                Text("$128.45")
                    .privacySensitive()
                    .font(.title2)
                    .foregroundColor(Color.gray)
            }
        }
    }
}

关键点:

  • .privacySensitive() 标记可能包含敏感信息的视图
  • 系统会在锁屏、隐私模式等环境下自动用占位符替代
  • 占位符通常是圆点、方块或通用图标
  • 不需要手动判断当前是否处于隐私模式

WidgetBundle 组织多个 Widget

23:08)一个 App 可以提供多个 Widget,通过 WidgetBundle 统一管理。

struct IndividualSymbolWidget: Widget {
    var body: some WidgetConfiguration {
        // 配置
    }
}

struct StocksOverviewWidget: Widget {
    var body: some WidgetConfiguration {
        // 配置
    }
}

@main
struct MyWidgetBundle: WidgetBundle {
    var body: some Widget {
        // 这里的顺序决定了 Widget Gallery 中的显示顺序
        IndividualSymbolWidget()
        StocksOverviewWidget()
    }
}

关键点:

  • @main 标记的 WidgetBundle 是 Widget 扩展的入口点
  • Bundle 中 Widget 的顺序与 Widget Gallery 中的一致
  • 每个 Widget 可以有独立的配置和 TimelineProvider

静态配置与 Intent 配置

25:43)Widget 有两种配置方式:静态配置和 Intent 配置。

// 静态配置 - 适合不需要用户自定义的 Widget
@main
public struct SampleWidget: Widget {
    public var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: "com.sample.myStaticSampleWidgetKind",
            provider: Provider()
        ) { entry in
            SampleWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

public struct Provider: TimelineProvider {
    public func timeline(with context: Context,
                         completion: @escaping (Timeline<Entry>) -> ()) {
        let entry = SimpleEntry(date: Date())
        completion(timeline)
    }
}
// Intent 配置 - 适合需要用户自定义的 Widget
@main
public struct SampleWidget: Widget {
    public var body: some WidgetConfiguration {
        IntentConfiguration(
            kind: "com.sample.myIntentSampleWidgetKind",
            intent: SampleConfigurationIntent.self,
            provider: Provider()
        ) { entry in
            SampleWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

public struct Provider: IntentTimelineProvider {
    public func timeline(for configuration: SampleConfigurationIntent,
                        with context: Context,
                        completion: @escaping (Timeline<Entry>) -> ()) {
        let entry = SimpleEntry(date: Date(), configuration: configuration)
        completion(timeline)
    }
}

关键点:

  • StaticConfiguration 使用 TimelineProvider,适合内容固定的 Widget
  • IntentConfiguration 使用 IntentTimelineProvider,需要传递用户配置的 Intent
  • Intent 类型需要在 .intentdefinition 文件中定义
  • 用户长按 Widget 可以打开配置界面,修改 Intent 参数

核心启发

  1. 多城市天气 Widget

    • 做什么:天气 Widget 让用户选择关注的城市数量和排序
    • 为什么值得做IntentConfiguration 让每个用户都能定制自己的天气面板
    • 怎么开始:在 .intentdefinition 中定义 CityConfigurationIntent,包含城市列表参数,Provider 根据用户选择的城市生成对应的 Timeline
  2. 个人财务仪表盘

    • 做什么:银行余额 Widget 在锁屏时自动遮挡具体金额,只显示”有余额”或余额范围
    • 为什么值得做.privacySensitive() 一行代码就能保护敏感信息
    • 怎么开始:在显示金额的 Text 视图上添加 .privacySensitive(),系统自动处理遮挡逻辑
  3. 智能堆叠自适应布局

    • 做什么:新闻 Widget 在主屏幕显示标题摘要,在智能堆叠中只显示分类图标和头条标题
    • 为什么值得做widgetFamily 环境值可以判断当前尺寸,动态调整信息密度
    • 怎么开始:用 @Environment(\.widgetFamily) 获取当前尺寸,@Environment(\.isPreview) 判断是否在预览中,根据环境返回不同的视图
  4. 多股票跟踪器

    • 做什么:股票 Widget 让用户添加关注的多只股票,按用户指定的顺序显示
    • 为什么值得做:Intent 支持数组参数,用户可以自由组合自己关心的股票
    • 怎么开始:定义 StockSymbolsIntent 包含 [String] 类型的股票代码数组,Provider 根据数组内容获取实时报价
  5. 分时段日程提醒

    • 做什么:日历 Widget 根据时间段显示不同内容:早上显示今日行程,下午显示通勤路况,晚上显示明天的天气
    • 为什么值得做TimelineEntry 可以为不同时间点设置不同的内容
    • 怎么开始:在 Timeline 中为不同的时间点生成不同的 Entry,每个 Entry 包含该时间段最相关的信息

关联 Session

评论

GitHub Issues · utterances