axiom-extensions-widgets-ref
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseExtensions & Widgets API Reference
扩展与小组件API参考
Overview
概述
This skill provides comprehensive API reference for Apple's widget and extension ecosystem:
- Standard Widgets (iOS 14+) — Home Screen, Lock Screen, StandBy widgets
- Interactive Widgets (iOS 17+) — Buttons and toggles with App Intents
- Live Activities (iOS 16.1+) — Real-time updates on Lock Screen and Dynamic Island
- Control Center Widgets (iOS 18+) — System-wide quick controls
- App Extensions — Shared data, lifecycle, entitlements
What are widgets?: Widgets are SwiftUI views that display timely, relevant information from your app. Unlike live app views, widgets are archived snapshots rendered on a timeline and displayed by the system.
What are extensions?: App extensions are separate executables bundled with your app that run in sandboxed environments with limited resources and capabilities.
本技能提供了苹果小组件与扩展生态的全面API参考:
- 标准小组件(iOS 14+)——主屏幕、锁屏、StandBy小组件
- 交互式小组件(iOS 17+)——支持App Intents的按钮与开关
- Live Activities(iOS 16.1+)——锁屏与Dynamic Island上的实时更新
- 控制中心小组件(iOS 18+)——系统级快捷控件
- 应用扩展——数据共享、生命周期、权限配置
什么是小组件?:小组件是SwiftUI视图,用于展示来自应用的及时、相关信息。与应用的实时视图不同,小组件是归档快照,按时间线渲染并由系统展示。
什么是扩展?:应用扩展是与主应用绑定的独立可执行文件,在沙箱环境中运行,资源与能力受限。
When to Use This Skill
何时使用本技能
✅ Use this skill when:
- Implementing any type of widget (Home Screen, Lock Screen, StandBy)
- Creating Live Activities for ongoing events
- Building Control Center controls
- Sharing data between app and extensions
- Understanding widget timelines and refresh policies
- Integrating widgets with App Intents
- Supporting watchOS or visionOS widgets
❌ Do NOT use this skill for:
- Pure App Intents questions (use app-intents-ref skill)
- SwiftUI layout issues (use swiftui-layout skill)
- Performance optimization (use swiftui-performance skill)
- Debugging crashes (use xcode-debugging skill)
✅ 在以下场景使用本技能:
- 实现任意类型的小组件(主屏幕、锁屏、StandBy)
- 为持续事件创建Live Activities
- 构建控制中心控件
- 在应用与扩展之间共享数据
- 理解小组件时间线与刷新策略
- 集成小组件与App Intents
- 支持watchOS或visionOS小组件
❌ 请勿在以下场景使用本技能:
- 纯App Intents相关问题(使用app-intents-ref技能)
- SwiftUI布局问题(使用swiftui-layout技能)
- 性能优化问题(使用swiftui-performance技能)
- 崩溃调试问题(使用xcode-debugging技能)
Related Skills
相关技能
- app-intents-ref — App Intents for interactive widgets and configuration
- swift-concurrency — Async/await patterns for widget data loading
- swiftui-performance — Optimizing widget rendering
- swiftui-layout — Complex widget layouts
- extensions-widgets — Discipline skill with anti-patterns and debugging
- app-intents-ref——用于交互式小组件与配置的App Intents
- swift-concurrency——小组件数据加载的Async/await模式
- swiftui-performance——优化小组件渲染性能
- swiftui-layout——复杂小组件布局
- extensions-widgets——包含反模式与调试方法的专项技能
Key Terminology
关键术语
Timeline — A series of entries that define when and what content your widget displays. The system automatically shows the appropriate entry at each specified time.
TimelineProvider — Protocol you implement to supply timeline entries to the system. Includes methods for placeholder, snapshot, and actual timeline generation.
TimelineEntry — A struct containing your widget's data and the date when it should be displayed. Each entry is like a "snapshot" of your widget at a specific time.
Timeline Budget — The daily limit (40-70) of how many times the system will request new timelines for your widget. Helps conserve battery.
Budget-Exempt — Timeline reloads that don't count against your daily budget (user-initiated, app foregrounding, system-initiated).
Widget Family — The size/shape of a widget (systemSmall, systemMedium, accessoryCircular, etc.). Your view adapts based on the family.
App Groups — An entitlement that allows your app and extensions to share data through a common container. Required for widgets to access app data.
ActivityAttributes — Defines both static data (set once when Live Activity starts) and dynamic ContentState (updated throughout activity lifecycle).
ContentState — The part of ActivityAttributes that changes during a Live Activity's lifetime. Must be under 4KB total.
Dynamic Island — iPhone 14 Pro+ feature where Live Activities appear around the TrueDepth camera. Has three sizes: compact, minimal, and expanded.
ControlWidget — iOS 18+ feature allowing widgets to appear in Control Center, Lock Screen, and Action Button for quick actions.
Concentric Alignment — Design principle for Dynamic Island content where visual mass (centroid) nestles inside the Island's rounded walls with even margins.
Visual Mass (Centroid) — The perceived "weight" center of your content. In Dynamic Island, this should align with the Island's shape for proper fit.
Supplemental Activity Families — Enables Live Activities to appear on Apple Watch or CarPlay in addition to iPhone.
时间线(Timeline)——一系列条目,定义小组件展示内容的时间与内容。系统会在指定时间自动展示相应的条目。
TimelineProvider——你需要实现的协议,用于向系统提供时间线条目,包含占位符、快照与实际时间线生成的方法。
TimelineEntry——包含小组件数据与展示时间的结构体。每个条目就像小组件在特定时间的一张"快照"。
时间线预算(Timeline Budget)——系统每天请求新时间线的次数上限(40-70次),用于节省电量。
免预算(Budget-Exempt)——不计入每日预算的时间线重载(用户触发、应用前台启动、系统触发)。
小组件类型(Widget Family)——小组件的尺寸/形状(systemSmall、systemMedium、accessoryCircular等)。你的视图会根据类型自适应。
App Groups——允许应用与扩展通过公共容器共享数据的权限,是小组件访问应用数据的必要配置。
ActivityAttributes——定义Live Activity的静态数据(启动时设置一次)与动态ContentState(在活动生命周期中更新)。
ContentState——ActivityAttributes中在Live Activity生命周期内会变化的部分,总大小必须不超过4KB。
Dynamic Island——iPhone 14 Pro+的功能,Live Activities会在原深感摄像头周围展示,有紧凑、最小化、展开三种尺寸。
ControlWidget——iOS 18+的功能,允许小组件出现在控制中心、锁屏与动作按钮中,用于快捷操作。
同心对齐(Concentric Alignment)——Dynamic Island内容的设计原则,视觉重心(质心)应嵌入Island的圆角边框内,保持均匀边距。
视觉重心(Visual Mass/Centroid)——内容的感知"重量"中心。在Dynamic Island中,它应与Island的形状对齐以实现合适的适配。
补充活动类型(Supplemental Activity Families)——允许Live Activities除了在iPhone上展示外,还能在Apple Watch或CarPlay上展示。
Part 1: Standard Widgets (iOS 14+)
第一部分:标准小组件(iOS 14+)
Widget Configuration Types
小组件配置类型
StaticConfiguration
StaticConfiguration
For widgets that don't require user configuration.
swift
@main
struct MyWidget: Widget {
let kind: String = "MyWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
MyWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This widget displays...")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}适用于无需用户配置的小组件。
swift
@main
struct MyWidget: Widget {
let kind: String = "MyWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
MyWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This widget displays...")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}AppIntentConfiguration (iOS 17+)
AppIntentConfiguration(iOS 17+)
For widgets with user configuration using App Intents.
swift
struct MyConfigurableWidget: Widget {
let kind: String = "MyConfigurableWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kind,
intent: SelectProjectIntent.self,
provider: Provider()
) { entry in
MyWidgetEntryView(entry: entry)
}
.configurationDisplayName("Project Status")
.description("Shows your selected project")
}
}Migration from IntentConfiguration: iOS 16 and earlier used with SiriKit intents. Migrate to for iOS 17+.
IntentConfigurationAppIntentConfiguration适用于使用App Intents进行用户配置的小组件。
swift
struct MyConfigurableWidget: Widget {
let kind: String = "MyConfigurableWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kind,
intent: SelectProjectIntent.self,
provider: Provider()
) { entry in
MyWidgetEntryView(entry: entry)
}
.configurationDisplayName("Project Status")
.description("Shows your selected project")
}
}从IntentConfiguration迁移:iOS 16及更早版本使用搭配SiriKit intents,iOS 17+需迁移至。
IntentConfigurationAppIntentConfigurationActivityConfiguration
ActivityConfiguration
For Live Activities (covered in Live Activities section).
用于Live Activities(在Live Activities章节详细介绍)。
Choosing the Right Configuration
选择合适的配置
Decision Tree:
Does your widget need user configuration?
├─ NO → Use StaticConfiguration
│ └─ Example: Weather widget for current location
│
└─ YES → Need configuration
├─ Simple static options (no dynamic data)?
│ └─ Use AppIntentConfiguration with WidgetConfigurationIntent
│ └─ Example: Timer with preset durations (5, 10, 15 minutes)
│
└─ Dynamic options (projects, contacts, playlists)?
└─ Use AppIntentConfiguration + EntityQuery
└─ Example: Project status widget showing user's projectsConfiguration Type Comparison:
| Configuration | Use When | Example |
|---|---|---|
| StaticConfiguration | No user customization needed | Weather for current location, battery status |
| AppIntentConfiguration (simple) | Fixed list of options | Timer presets, theme selection |
| AppIntentConfiguration (EntityQuery) | Dynamic list from app data | Project picker, contact picker, playlist selector |
| ActivityConfiguration | Live ongoing events | Delivery tracking, workout progress, sports scores |
决策树:
你的小组件是否需要用户配置?
├─ 否 → 使用StaticConfiguration
│ └─ 示例:当前位置的天气小组件
│
└─ 是 → 需要配置
├─ 简单静态选项(无动态数据)?
│ └─ 使用AppIntentConfiguration搭配WidgetConfigurationIntent
│ └─ 示例:带有预设时长(5、10、15分钟)的计时器
│
└─ 动态选项(项目、联系人、播放列表)?
└─ 使用AppIntentConfiguration + EntityQuery
└─ 示例:展示用户项目的项目状态小组件配置类型对比:
| 配置类型 | 使用场景 | 示例 |
|---|---|---|
| StaticConfiguration | 无需用户自定义 | 当前位置天气、电池状态 |
| AppIntentConfiguration(简单型) | 固定选项列表 | 计时器预设、主题选择 |
| AppIntentConfiguration(EntityQuery) | 来自应用数据的动态列表 | 项目选择器、联系人选择器、播放列表选择器 |
| ActivityConfiguration | 正在进行的实时事件 | 配送跟踪、锻炼进度、体育比分 |
Widget Families
小组件类型
System Families (Home Screen)
系统类型(主屏幕)
| Family | Size (points) | iOS Version | Use Case |
|---|---|---|---|
| ~170×170 | 14+ | Single piece of info, icon |
| ~360×170 | 14+ | Multiple data points, chart |
| ~360×380 | 14+ | Detailed view, list |
| ~720×380 | 15+ (iPad only) | Rich layouts, multiple views |
| 类型 | 尺寸(点) | iOS版本 | 使用场景 |
|---|---|---|---|
| ~170×170 | 14+ | 单一信息、图标 |
| ~360×170 | 14+ | 多个数据点、图表 |
| ~360×380 | 14+ | 详细视图、列表 |
| ~720×380 | 15+(仅iPad) | 丰富布局、多视图 |
Accessory Families (Lock Screen, iOS 16+)
配件类型(锁屏,iOS 16+)
| Family | Location | Size | Content |
|---|---|---|---|
| Circular complication | ~48×48pt | Icon or gauge |
| Above clock | ~160×72pt | Text + icon |
| Above date | Single line | Text only |
| 类型 | 位置 | 尺寸 | 内容 |
|---|---|---|---|
| 圆形复杂控件 | ~48×48pt | 图标或仪表盘 |
| 时钟上方 | ~160×72pt | 文字+图标 |
| 日期上方 | 单行 | 仅文字 |
Example: Supporting Multiple Families
示例:支持多种类型
swift
struct MyWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "MyWidget", provider: Provider()) { entry in
if #available(iOSApplicationExtension 16.0, *) {
switch entry.family {
case .systemSmall:
SmallWidgetView(entry: entry)
case .systemMedium:
MediumWidgetView(entry: entry)
case .accessoryCircular:
CircularWidgetView(entry: entry)
case .accessoryRectangular:
RectangularWidgetView(entry: entry)
default:
Text("Unsupported")
}
} else {
LegacyWidgetView(entry: entry)
}
}
.supportedFamilies([
.systemSmall,
.systemMedium,
.accessoryCircular,
.accessoryRectangular
])
}
}swift
struct MyWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "MyWidget", provider: Provider()) { entry in
if #available(iOSApplicationExtension 16.0, *) {
switch entry.family {
case .systemSmall:
SmallWidgetView(entry: entry)
case .systemMedium:
MediumWidgetView(entry: entry)
case .accessoryCircular:
CircularWidgetView(entry: entry)
case .accessoryRectangular:
RectangularWidgetView(entry: entry)
default:
Text("Unsupported")
}
} else {
LegacyWidgetView(entry: entry)
}
}
.supportedFamilies([
.systemSmall,
.systemMedium,
.accessoryCircular,
.accessoryRectangular
])
}
}Timeline System
时间线系统
TimelineProvider Protocol
TimelineProvider协议
Provides entries that define when the system should render your widget.
swift
struct Provider: TimelineProvider {
// Placeholder while loading
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), emoji: "😀")
}
// Shown in widget gallery
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), emoji: "📷")
completion(entry)
}
// Actual timeline
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let currentDate = Date()
// Create entry every hour for 5 hours
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, emoji: "⏰")
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}提供定义系统应何时渲染小组件的条目。
swift
struct Provider: TimelineProvider {
// 加载时的占位符
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), emoji: "😀")
}
// 小组件图库中展示的快照
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), emoji: "📷")
completion(entry)
}
// 实际时间线
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let currentDate = Date()
// 每小时创建一个条目,共5小时
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, emoji: "⏰")
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}TimelineReloadPolicy
TimelineReloadPolicy
Controls when the system requests a new timeline.
| Policy | Behavior |
|---|---|
| Reload after last entry |
| Reload at specific date |
| No automatic reload (manual only) |
控制系统何时请求新时间线。
| 策略 | 行为 |
|---|---|
| 最后一个条目展示后重载 |
| 在指定日期重载 |
| 不自动重载(仅手动重载) |
Manual Reload
手动重载
swift
import WidgetKit
// Reload all widgets of this kind
WidgetCenter.shared.reloadAllTimelines()
// Reload specific kind
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")swift
import WidgetKit
// 重载该类型的所有小组件
WidgetCenter.shared.reloadAllTimelines()
// 重载指定类型的小组件
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")Refresh Budgets
刷新预算
Daily budget: 40-70 timeline reloads per day (varies by system load and user engagement)
每日预算:系统每天会为你的小组件请求新时间线的次数上限为40-70次(根据系统负载与用户参与度有所不同)
Budget-Exempt Scenarios
免预算场景
These do NOT count against your budget:
- User explicitly reloads (pull-to-refresh on Home Screen)
- App is foregrounded
- User adds widget to Home Screen
- System-initiated reloads (e.g., after reboot)
以下场景的重载不计入每日预算:
- 用户显式重载(主屏幕下拉刷新)
- 应用进入前台
- 用户添加小组件到主屏幕
- 系统触发的重载(如重启后)
Best Practices
最佳实践
swift
// ✅ GOOD: Strategic intervals (15-60 min)
let entries = (0..<8).map { offset in
let date = Calendar.current.date(byAdding: .minute, value: offset * 15, to: now)!
return SimpleEntry(date: date, data: data)
}
// ❌ BAD: Too frequent (1 min) - will exhaust budget
let entries = (0..<60).map { offset in
let date = Calendar.current.date(byAdding: .minute, value: offset, to: now)!
return SimpleEntry(date: date, data: data)
}swift
// ✅ 推荐:合理的时间间隔(15-60分钟)
let entries = (0..<8).map { offset in
let date = Calendar.current.date(byAdding: .minute, value: offset * 15, to: now)!
return SimpleEntry(date: date, data: data)
}
// ❌ 不推荐:过于频繁(1分钟)——会耗尽预算
let entries = (0..<60).map { offset in
let date = Calendar.current.date(byAdding: .minute, value: offset, to: now)!
return SimpleEntry(date: date, data: data)
}Performance Implications
性能影响
Memory Limits
内存限制
Widget extensions have strict memory limits:
- ~30MB for standard widgets
- ~50MB for Live Activities
- System terminates extension if exceeded
Best practices:
swift
// ✅ GOOD: Load only what you need
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let data = loadRecentItems(limit: 10) // Limited dataset
let entries = generateEntries(from: data)
completion(Timeline(entries: entries, policy: .atEnd))
}
// ❌ BAD: Loading entire database
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let allData = database.loadAllItems() // Thousands of items = memory spike
// ...
}小组件扩展有严格的内存限制:
- 标准小组件约30MB
- Live Activities约50MB
- 超过限制系统会终止扩展
最佳实践:
swift
// ✅ 推荐:仅加载所需数据
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let data = loadRecentItems(limit: 10) // 限制数据集大小
let entries = generateEntries(from: data)
completion(Timeline(entries: entries, policy: .atEnd))
}
// ❌ 不推荐:加载整个数据库
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let allData = database.loadAllItems() // 数千条数据会导致内存峰值
// ...
}Network Requests
网络请求
Never make network requests in widget views - they won't complete before rendering.
swift
// ❌ CRITICAL ERROR: Network in view
struct MyWidgetView: View {
var body: some View {
VStack {
Text("Weather")
}
.onAppear {
Task {
// This will NOT work - view is already rendered
let weather = try? await fetchWeather()
}
}
}
}
// ✅ CORRECT: Network in timeline provider
struct Provider: TimelineProvider {
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
Task {
// Fetch data here, before rendering
let weather = try await fetchWeather()
let entry = SimpleEntry(date: Date(), weather: weather)
completion(Timeline(entries: [entry], policy: .atEnd))
}
}
}绝对不要在小组件视图中发起网络请求——它们会在渲染完成前无法执行完毕。
swift
// ❌ 严重错误:在视图中发起网络请求
struct MyWidgetView: View {
var body: some View {
VStack {
Text("Weather")
}
.onAppear {
Task {
// 这不会生效——视图已经渲染完成
let weather = try? await fetchWeather()
}
}
}
}
// ✅ 正确:在时间线提供者中发起网络请求
struct Provider: TimelineProvider {
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
Task {
// 在渲染前获取数据
let weather = try await fetchWeather()
let entry = SimpleEntry(date: Date(), weather: weather)
completion(Timeline(entries: [entry], policy: .atEnd))
}
}
}Timeline Generation Performance
时间线生成性能
Target: Complete in under 5 seconds
getTimeline()Strategies:
- Cache in main app - Precompute expensive operations
- Async/await - Don't block completion handler
- Limit entries - 10-20 entries maximum
- Minimal computation - Simple transformations only
swift
// ✅ GOOD: Fast timeline generation
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
// Read pre-computed data from shared container
let shared = UserDefaults(suiteName: "group.com.myapp")!
let cachedData = shared.data(forKey: "widgetData")
let entries = generateQuickEntries(from: cachedData)
completion(Timeline(entries: entries, policy: .after(Date().addingTimeInterval(3600))))
}
// ❌ BAD: Expensive operations in timeline
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
// Parsing large JSON, complex algorithms
let json = parseHugeJSON() // 10+ seconds
let analyzed = runMLModel(on: json) // 5+ seconds
// Widget will timeout and show placeholder
}目标:的执行时间控制在5秒以内
getTimeline()策略:
- 在主应用中缓存——预计算耗时操作
- 使用Async/await——不要阻塞完成处理程序
- 限制条目数量——最多10-20个条目
- 最小化计算——仅进行简单转换
swift
// ✅ 推荐:快速生成时间线
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
// 从共享容器读取预计算的数据
let shared = UserDefaults(suiteName: "group.com.myapp")!
let cachedData = shared.data(forKey: "widgetData")
let entries = generateQuickEntries(from: cachedData)
completion(Timeline(entries: entries, policy: .after(Date().addingTimeInterval(3600))))
}
// ❌ 不推荐:在时间线中执行耗时操作
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
// 解析大型JSON、复杂算法
let json = parseHugeJSON() // 10秒以上
let analyzed = runMLModel(on: json) // 5秒以上
// 小组件会超时并显示占位符
}Battery Impact
电池影响
Widget refresh = battery drain
| Refresh Strategy | Daily Budget Used | Battery Impact |
|---|---|---|
| Strategic (4x/hour) | ~48 reloads | Low |
| Aggressive (12x/hour) | Budget exhausted by 6 PM | High |
| On-demand only | 5-10 reloads | Minimal |
When to reload:
- ✅ Significant data change (order status update)
- ✅ User opens app (free reload)
- ✅ Time-based (hourly weather)
- ❌ Speculative updates (might change)
- ❌ Cosmetic changes (color theme)
小组件刷新会消耗电池
| 刷新策略 | 每日预算使用量 | 电池影响 |
|---|---|---|
| 合理策略(每小时4次) | ~48次重载 | 低 |
| 激进策略(每小时12次) | 预算在下午6点前耗尽 | 高 |
| 仅按需重载 | 5-10次重载 | 极小 |
何时重载:
- ✅ 数据发生重大变化(如订单状态更新)
- ✅ 用户打开应用(免费重载)
- ✅ 基于时间的更新(如每小时天气)
- ❌ 推测性更新(可能变化)
- ❌ cosmetic变化(如颜色主题)
View Rendering Performance
视图渲染性能
Widgets render frequently (every time user views Home Screen/Lock Screen)
swift
// ✅ GOOD: Simple, efficient views
struct MyWidgetView: View {
var entry: Provider.Entry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(entry.title)
.font(.headline)
Text(entry.subtitle)
.font(.caption)
}
.padding()
}
}
// ❌ BAD: Heavy view operations
struct MyWidgetView: View {
var entry: Provider.Entry
var body: some View {
VStack {
// Avoid expensive operations in view body
Text(entry.title)
// Don't compute in body - precompute in entry
ForEach(complexCalculation(entry.data)) { item in
Text(item.name)
}
}
}
func complexCalculation(_ data: [Item]) -> [ProcessedItem] {
// This runs on EVERY render
return data.map { /* expensive transform */ }
}
}Rule: Precompute everything in , keep views simple.
TimelineEntry小组件会频繁渲染(每次用户查看主屏幕/锁屏时)
swift
// ✅ 推荐:简单高效的视图
struct MyWidgetView: View {
var entry: Provider.Entry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(entry.title)
.font(.headline)
Text(entry.subtitle)
.font(.caption)
}
.padding()
}
}
// ❌ 不推荐:视图中执行 heavy 操作
struct MyWidgetView: View {
var entry: Provider.Entry
var body: some View {
VStack {
// 避免在视图体中执行耗时操作
Text(entry.title)
// 不要在视图体中计算——在entry中预计算
ForEach(complexCalculation(entry.data)) { item in
Text(item.name)
}
}
}
func complexCalculation(_ data: [Item]) -> [ProcessedItem] {
// 每次渲染都会执行
return data.map { /* 耗时转换 */ }
}
}规则:所有内容都在中预计算,保持视图简单。
TimelineEntryImage Performance
图片性能
swift
// ✅ GOOD: Asset catalog images (fast)
Image("icon-weather")
// ✅ GOOD: SF Symbols (fast)
Image(systemName: "cloud.rain.fill")
// ⚠️ ACCEPTABLE: Small images from shared container
if let imageData = Data(/* from shared container */),
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
}
// ❌ BAD: Remote images (won't load)
AsyncImage(url: URL(string: "https://...")) // Doesn't work in widgets
// ❌ BAD: Large images (memory spike)
Image(/* 4K resolution image */) // Will cause terminationswift
// ✅ 推荐:资源目录图片(快速)
Image("icon-weather")
// ✅ 推荐:SF Symbols(快速)
Image(systemName: "cloud.rain.fill")
// ⚠️ 可接受:来自共享容器的小图片
if let imageData = Data(/* from shared container */),
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
}
// ❌ 不推荐:远程图片(无法加载)
AsyncImage(url: URL(string: "https://...")) // 在小组件中无效
// ❌ 不推荐:大图片(内存峰值)
Image(/* 4K分辨率图片 */) // 会导致扩展被终止Part 2: Interactive Widgets (iOS 17+)
第二部分:交互式小组件(iOS 17+)
Button and Toggle
按钮与开关
Interactive widgets use SwiftUI and with App Intents.
ButtonToggle交互式小组件使用搭配App Intents的SwiftUI 和。
ButtonToggleButton with App Intent
搭配App Intent的按钮
swift
struct MyWidgetView: View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.count)
Button(intent: IncrementIntent()) {
Label("Increment", systemImage: "plus.circle")
}
}
}
}
struct IncrementIntent: AppIntent {
static var title: LocalizedStringResource = "Increment Counter"
func perform() async throws -> some IntentResult {
// Update shared data using App Groups
let shared = UserDefaults(suiteName: "group.com.myapp")!
let count = shared.integer(forKey: "count")
shared.set(count + 1, forKey: "count")
return .result()
}
}swift
struct MyWidgetView: View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.count)
Button(intent: IncrementIntent()) {
Label("Increment", systemImage: "plus.circle")
}
}
}
}
struct IncrementIntent: AppIntent {
static var title: LocalizedStringResource = "Increment Counter"
func perform() async throws -> some IntentResult {
// 使用App Groups更新共享数据
let shared = UserDefaults(suiteName: "group.com.myapp")!
let count = shared.integer(forKey: "count")
shared.set(count + 1, forKey: "count")
return .result()
}
}Toggle with App Intent
搭配App Intent的开关
swift
struct ToggleFeatureIntent: AppIntent {
static var title: LocalizedStringResource = "Toggle Feature"
@Parameter(title: "Enabled")
var enabled: Bool
func perform() async throws -> some IntentResult {
// Update shared data using App Groups
let shared = UserDefaults(suiteName: "group.com.myapp")!
shared.set(enabled, forKey: "featureEnabled")
return .result()
}
}
struct MyWidgetView: View {
@State private var isEnabled: Bool = false
var body: some View {
Toggle(isOn: $isEnabled) {
Text("Feature")
}
.onChange(of: isEnabled) { newValue in
Task {
try? await ToggleFeatureIntent(enabled: newValue).perform()
}
}
}
}swift
struct ToggleFeatureIntent: AppIntent {
static var title: LocalizedStringResource = "Toggle Feature"
@Parameter(title: "Enabled")
var enabled: Bool
func perform() async throws -> some IntentResult {
// 使用App Groups更新共享数据
let shared = UserDefaults(suiteName: "group.com.myapp")!
shared.set(enabled, forKey: "featureEnabled")
return .result()
}
}
struct MyWidgetView: View {
@State private var isEnabled: Bool = false
var body: some View {
Toggle(isOn: $isEnabled) {
Text("Feature")
}
.onChange(of: isEnabled) { newValue in
Task {
try? await ToggleFeatureIntent(enabled: newValue).perform()
}
}
}
}invalidatableContent Modifier
invalidatableContent修饰符
Provides visual feedback during App Intent execution.
swift
struct MyWidgetView: View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.status)
.invalidatableContent() // Dims during intent execution
Button(intent: RefreshIntent()) {
Image(systemName: "arrow.clockwise")
}
}
}
}Effect: Content with becomes slightly transparent while the associated intent executes, providing user feedback.
.invalidatableContent()在App Intent执行期间提供视觉反馈。
swift
struct MyWidgetView: View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.status)
.invalidatableContent() // 执行Intent时变暗
Button(intent: RefreshIntent()) {
Image(systemName: "arrow.clockwise")
}
}
}
}效果:带有的内容在关联Intent执行期间会略微透明,为用户提供反馈。
.invalidatableContent()Animation System
动画系统
contentTransition for Numeric Text
数字文本的contentTransition
swift
Text("\(entry.value)")
.contentTransition(.numericText(value: Double(entry.value)))Effect: Numbers smoothly count up or down instead of instantly changing.
swift
Text("\(entry.value)")
.contentTransition(.numericText(value: Double(entry.value)))效果:数字会平滑地递增或递减,而非瞬间变化。
View Transitions
视图过渡
swift
VStack {
if entry.showDetail {
DetailView()
.transition(.scale.combined(with: .opacity))
}
}
.animation(.spring(response: 0.3), value: entry.showDetail)swift
VStack {
if entry.showDetail {
DetailView()
.transition(.scale.combined(with: .opacity))
}
}
.animation(.spring(response: 0.3), value: entry.showDetail)Part 3: Configurable Widgets (iOS 17+)
第三部分:可配置小组件(iOS 17+)
WidgetConfigurationIntent
WidgetConfigurationIntent
Define configuration parameters for your widget.
swift
import AppIntents
struct SelectProjectIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Select Project"
static var description = IntentDescription("Choose which project to display")
@Parameter(title: "Project")
var project: ProjectEntity?
// Provide default value
static var parameterSummary: some ParameterSummary {
Summary("Show \(\.$project)")
}
}定义小组件的配置参数。
swift
import AppIntents
struct SelectProjectIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Select Project"
static var description = IntentDescription("Choose which project to display")
@Parameter(title: "Project")
var project: ProjectEntity?
// 提供默认值
static var parameterSummary: some ParameterSummary {
Summary("Show \(\.$project)")
}
}Entity and EntityQuery
Entity与EntityQuery
Provide dynamic options for configuration.
swift
struct ProjectEntity: AppEntity {
var id: String
var name: String
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project")
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)")
}
}
struct ProjectQuery: EntityQuery {
func entities(for identifiers: [String]) async throws -> [ProjectEntity] {
// Return projects matching these IDs
return await ProjectStore.shared.projects(withIDs: identifiers)
}
func suggestedEntities() async throws -> [ProjectEntity] {
// Return all available projects
return await ProjectStore.shared.allProjects()
}
}为配置提供动态选项。
swift
struct ProjectEntity: AppEntity {
var id: String
var name: String
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project")
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)")
}
}
struct ProjectQuery: EntityQuery {
func entities(for identifiers: [String]) async throws -> [ProjectEntity] {
// 返回匹配这些ID的项目
return await ProjectStore.shared.projects(withIDs: identifiers)
}
func suggestedEntities() async throws -> [ProjectEntity] {
// 返回所有可用项目
return await ProjectStore.shared.allProjects()
}
}Using Configuration in Provider
在Provider中使用配置
swift
struct Provider: AppIntentTimelineProvider {
func timeline(for configuration: SelectProjectIntent, in context: Context) async -> Timeline<SimpleEntry> {
let project = configuration.project // Use selected project
let entries = await generateEntries(for: project)
return Timeline(entries: entries, policy: .atEnd)
}
}swift
struct Provider: AppIntentTimelineProvider {
func timeline(for configuration: SelectProjectIntent, in context: Context) async -> Timeline<SimpleEntry> {
let project = configuration.project // 使用选中的项目
let entries = await generateEntries(for: project)
return Timeline(entries: entries, policy: .atEnd)
}
}Part 4: Live Activities (iOS 16.1+)
第四部分:Live Activities(iOS 16.1+)
ActivityAttributes
ActivityAttributes
Defines static and dynamic data for a Live Activity.
swift
import ActivityKit
struct PizzaDeliveryAttributes: ActivityAttributes {
// Static data - set when activity starts, never changes
struct ContentState: Codable, Hashable {
// Dynamic data - updated throughout activity lifecycle
var status: DeliveryStatus
var estimatedDeliveryTime: Date
var driverName: String?
}
// Static attributes
var orderNumber: String
var pizzaType: String
}Key constraint: total data size must be under 4KB to start successfully.
ActivityAttributes定义Live Activity的静态与动态数据。
swift
import ActivityKit
struct PizzaDeliveryAttributes: ActivityAttributes {
// 静态数据——启动时设置,永不改变
struct ContentState: Codable, Hashable {
// 动态数据——在活动生命周期中更新
var status: DeliveryStatus
var estimatedDeliveryTime: Date
var driverName: String?
}
// 静态属性
var orderNumber: String
var pizzaType: String
}关键限制:的总数据大小必须小于4KB才能成功启动。
ActivityAttributesStarting Activities
启动活动
Request Authorization
请求授权
swift
import ActivityKit
let authorizationInfo = ActivityAuthorizationInfo()
let areActivitiesEnabled = authorizationInfo.areActivitiesEnabledswift
import ActivityKit
let authorizationInfo = ActivityAuthorizationInfo()
let areActivitiesEnabled = authorizationInfo.areActivitiesEnabledStart an Activity
启动活动
swift
let attributes = PizzaDeliveryAttributes(
orderNumber: "12345",
pizzaType: "Pepperoni"
)
let initialState = PizzaDeliveryAttributes.ContentState(
status: .preparing,
estimatedDeliveryTime: Date().addingTimeInterval(30 * 60)
)
let activity = try Activity.request(
attributes: attributes,
content: ActivityContent(state: initialState, staleDate: nil),
pushType: nil // or .token for push notifications
)swift
let attributes = PizzaDeliveryAttributes(
orderNumber: "12345",
pizzaType: "Pepperoni"
)
let initialState = PizzaDeliveryAttributes.ContentState(
status: .preparing,
estimatedDeliveryTime: Date().addingTimeInterval(30 * 60)
)
let activity = try Activity.request(
attributes: attributes,
content: ActivityContent(state: initialState, staleDate: nil),
pushType: nil // 或使用.token获取推送通知
)Error Handling
错误处理
Common Activity Errors
常见活动错误
swift
import ActivityKit
func startDeliveryActivity(order: Order) {
// Check authorization first
let authInfo = ActivityAuthorizationInfo()
guard authInfo.areActivitiesEnabled else {
print("Live Activities not enabled by user")
return
}
let attributes = PizzaDeliveryAttributes(
orderNumber: order.id,
pizzaType: order.pizzaType
)
let initialState = PizzaDeliveryAttributes.ContentState(
status: .preparing,
estimatedDeliveryTime: order.estimatedTime
)
do {
let activity = try Activity.request(
attributes: attributes,
content: ActivityContent(state: initialState, staleDate: nil),
pushType: .token
)
// Store activity ID for later updates
UserDefaults.shared.set(activity.id, forKey: "currentDeliveryActivityID")
} catch let error as ActivityAuthorizationError {
// User denied Live Activities permission
print("Authorization error: \(error.localizedDescription)")
} catch let error as ActivityError {
switch error {
case .dataTooLarge:
// ActivityAttributes exceeds 4KB
print("Activity data too large - reduce attribute size")
case .tooManyActivities:
// System limit reached (typically 2-3 simultaneous)
print("Too many active Live Activities")
default:
print("Activity error: \(error.localizedDescription)")
}
} catch {
print("Unexpected error: \(error)")
}
}swift
import ActivityKit
func startDeliveryActivity(order: Order) {
// 先检查授权
let authInfo = ActivityAuthorizationInfo()
guard authInfo.areActivitiesEnabled else {
print("Live Activities未被用户启用")
return
}
let attributes = PizzaDeliveryAttributes(
orderNumber: order.id,
pizzaType: order.pizzaType
)
let initialState = PizzaDeliveryAttributes.ContentState(
status: .preparing,
estimatedDeliveryTime: order.estimatedTime
)
do {
let activity = try Activity.request(
attributes: attributes,
content: ActivityContent(state: initialState, staleDate: nil),
pushType: .token
)
// 存储活动ID以便后续更新
UserDefaults.shared.set(activity.id, forKey: "currentDeliveryActivityID")
} catch let error as ActivityAuthorizationError {
// 用户拒绝了Live Activities权限
print("授权错误:\(error.localizedDescription)")
} catch let error as ActivityError {
switch error {
case .dataTooLarge:
// ActivityAttributes超过4KB
print("活动数据过大——减小属性大小")
case .tooManyActivities:
// 达到系统限制(通常为2-3个同时活动)
print("活动数量过多")
default:
print("活动错误:\(error.localizedDescription)")
}
} catch {
print("意外错误:\(error)")
}
}Safely Updating Activities
安全更新活动
swift
func updateActivity(newStatus: DeliveryStatus) async {
// Find active activity
guard let activityID = UserDefaults.shared.string(forKey: "currentDeliveryActivityID"),
let activity = Activity<PizzaDeliveryAttributes>.activities.first(where: { $0.id == activityID })
else {
print("No active delivery activity found")
return
}
let updatedState = PizzaDeliveryAttributes.ContentState(
status: newStatus,
estimatedDeliveryTime: Date().addingTimeInterval(10 * 60),
driverName: "John"
)
// Await the update result
let updateTask = Task {
await activity.update(
ActivityContent(state: updatedState, staleDate: nil)
)
}
await updateTask.value
}swift
func updateActivity(newStatus: DeliveryStatus) async {
// 查找活动
guard let activityID = UserDefaults.shared.string(forKey: "currentDeliveryActivityID"),
let activity = Activity<PizzaDeliveryAttributes>.activities.first(where: { $0.id == activityID })
else {
print("未找到活跃的配送活动")
return
}
let updatedState = PizzaDeliveryAttributes.ContentState(
status: newStatus,
estimatedDeliveryTime: Date().addingTimeInterval(10 * 60),
driverName: "John"
)
// 等待更新结果
let updateTask = Task {
await activity.update(
ActivityContent(state: updatedState, staleDate: nil)
)
}
await updateTask.value
}Handling Activity Lifecycle
处理活动生命周期
swift
class DeliveryManager {
private var activityTask: Task<Void, Never>?
func monitorActivity(_ activity: Activity<PizzaDeliveryAttributes>) {
// Cancel previous monitoring
activityTask?.cancel()
// Monitor activity state
activityTask = Task {
for await state in activity.activityStateUpdates {
switch state {
case .active:
print("Activity is active")
case .ended:
print("Activity ended by system")
// Clean up
UserDefaults.shared.removeObject(forKey: "currentDeliveryActivityID")
case .dismissed:
print("Activity dismissed by user")
// Clean up
UserDefaults.shared.removeObject(forKey: "currentDeliveryActivityID")
case .stale:
print("Activity marked stale")
@unknown default:
break
}
}
}
}
deinit {
activityTask?.cancel()
}
}swift
class DeliveryManager {
private var activityTask: Task<Void, Never>?
func monitorActivity(_ activity: Activity<PizzaDeliveryAttributes>) {
// 取消之前的监控
activityTask?.cancel()
// 监控活动状态
activityTask = Task {
for await state in activity.activityStateUpdates {
switch state {
case .active:
print("活动处于活跃状态")
case .ended:
print("活动被系统结束")
// 清理
UserDefaults.shared.removeObject(forKey: "currentDeliveryActivityID")
case .dismissed:
print("活动被用户关闭")
// 清理
UserDefaults.shared.removeObject(forKey: "currentDeliveryActivityID")
case .stale:
print("活动被标记为过期")
@unknown default:
break
}
}
}
}
deinit {
activityTask?.cancel()
}
}Updating Activities
更新活动
Update with New Content
使用新内容更新
swift
let updatedState = PizzaDeliveryAttributes.ContentState(
status: .onTheWay,
estimatedDeliveryTime: Date().addingTimeInterval(10 * 60),
driverName: "John"
)
await activity.update(
ActivityContent(
state: updatedState,
staleDate: Date().addingTimeInterval(60) // Mark stale after 1 min
)
)swift
let updatedState = PizzaDeliveryAttributes.ContentState(
status: .onTheWay,
estimatedDeliveryTime: Date().addingTimeInterval(10 * 60),
driverName: "John"
)
await activity.update(
ActivityContent(
state: updatedState,
staleDate: Date().addingTimeInterval(60) // 1分钟后标记为过期
)
)Alert Configuration
提醒配置
swift
let updatedContent = ActivityContent(
state: updatedState,
staleDate: nil
)
await activity.update(updatedContent, alertConfiguration: AlertConfiguration(
title: "Pizza is here!",
body: "Your \(attributes.pizzaType) pizza has arrived",
sound: .default
))swift
let updatedContent = ActivityContent(
state: updatedState,
staleDate: nil
)
await activity.update(updatedContent, alertConfiguration: AlertConfiguration(
title: "Pizza is here!",
body: "Your \(attributes.pizzaType) pizza has arrived",
sound: .default
))Ending Activities
结束活动
Dismissal Policies
关闭策略
swift
// Immediate - removes instantly
await activity.end(nil, dismissalPolicy: .immediate)
// Default - stays for ~4 hours on Lock Screen
await activity.end(nil, dismissalPolicy: .default)
// After date - removes at specific time
let dismissTime = Date().addingTimeInterval(60 * 60) // 1 hour
await activity.end(nil, dismissalPolicy: .after(dismissTime))swift
// 立即关闭——立即移除
await activity.end(nil, dismissalPolicy: .immediate)
// 默认——在锁屏上保留约4小时
await activity.end(nil, dismissalPolicy: .default)
// 指定日期后关闭——在特定时间移除
let dismissTime = Date().addingTimeInterval(60 * 60) // 1小时后
await activity.end(nil, dismissalPolicy: .after(dismissTime))Final Content
最终内容
swift
let finalState = PizzaDeliveryAttributes.ContentState(
status: .delivered,
estimatedDeliveryTime: Date(),
driverName: "John"
)
await activity.end(
ActivityContent(state: finalState, staleDate: nil),
dismissalPolicy: .default
)swift
let finalState = PizzaDeliveryAttributes.ContentState(
status: .delivered,
estimatedDeliveryTime: Date(),
driverName: "John"
)
await activity.end(
ActivityContent(state: finalState, staleDate: nil),
dismissalPolicy: .default
)Push Notifications for Live Activities
Live Activities的推送通知
Request Push Token
请求推送令牌
swift
let activity = try Activity.request(
attributes: attributes,
content: initialContent,
pushType: .token // Request push token
)
// Monitor for push token
for await pushToken in activity.pushTokenUpdates {
let tokenString = pushToken.map { String(format: "%02x", $0) }.joined()
// Send to your server
await sendTokenToServer(tokenString, activityID: activity.id)
}swift
let activity = try Activity.request(
attributes: attributes,
content: initialContent,
pushType: .token // 请求推送令牌
)
// 监控推送令牌
for await pushToken in activity.pushTokenUpdates {
let tokenString = pushToken.map { String(format: "%02x", $0) }.joined()
// 发送到你的服务器
await sendTokenToServer(tokenString, activityID: activity.id)
}Frequent Push Updates (iOS 18.2+)
频繁推送更新(iOS 18.2+)
For scenarios requiring more frequent updates than standard push limits:
swift
let activity = try Activity.request(
attributes: attributes,
content: initialContent,
pushType: .token
)
// App needs "com.apple.developer.activity-push-notification-frequent-updates" entitlementStandard push limit: ~10-12 per hour
Frequent push entitlement: Significantly higher limit for live events (sports, stocks, etc.)
适用于需要比标准推送限制更频繁更新的场景:
swift
let activity = try Activity.request(
attributes: attributes,
content: initialContent,
pushType: .token
)
// 应用需要"com.apple.developer.activity-push-notification-frequent-updates"权限标准推送限制:每小时约10-12次
频繁推送权限:为实时事件(如体育赛事、股票等)提供更高的限制
Part 5: Dynamic Island (iOS 16.1+)
第五部分:Dynamic Island(iOS 16.1+)
Presentation Types
展示类型
Live Activities appear in the Dynamic Island with three size classes:
Live Activities在Dynamic Island中有三种尺寸类型:
Compact (Leading + Trailing)
紧凑(左侧+右侧)
Shown when another Live Activity is expanded or when multiple activities are active.
swift
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "timer")
}
DynamicIslandExpandedRegion(.trailing) {
Text("\(entry.timeRemaining)")
}
// ...
} compactLeading: {
Image(systemName: "timer")
} compactTrailing: {
Text("\(entry.timeRemaining)")
.frame(width: 40)
}当另一个Live Activities处于展开状态或有多个活动活跃时展示。
swift
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "timer")
}
DynamicIslandExpandedRegion(.trailing) {
Text("\(entry.timeRemaining)")
}
// ...
} compactLeading: {
Image(systemName: "timer")
} compactTrailing: {
Text("\(entry.timeRemaining)")
.frame(width: 40)
}Minimal
最小化
Shown when more than two Live Activities are active (circular avatar).
swift
DynamicIsland {
// ...
} minimal: {
Image(systemName: "timer")
.foregroundStyle(.tint)
}当有两个以上Live Activities活跃时展示(圆形头像)。
swift
DynamicIsland {
// ...
} minimal: {
Image(systemName: "timer")
.foregroundStyle(.tint)
}Expanded
展开
Shown when user long-presses the compact view.
swift
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "timer")
.font(.title)
}
DynamicIslandExpandedRegion(.trailing) {
VStack(alignment: .trailing) {
Text("\(entry.timeRemaining)")
.font(.title2.monospacedDigit())
Text("remaining")
.font(.caption)
}
}
DynamicIslandExpandedRegion(.center) {
// Optional center content
}
DynamicIslandExpandedRegion(.bottom) {
HStack {
Button(intent: PauseIntent()) {
Label("Pause", systemImage: "pause.fill")
}
Button(intent: StopIntent()) {
Label("Stop", systemImage: "stop.fill")
}
}
}
}当用户长按紧凑视图时展示。
swift
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "timer")
.font(.title)
}
DynamicIslandExpandedRegion(.trailing) {
VStack(alignment: .trailing) {
Text("\(entry.timeRemaining)")
.font(.title2.monospacedDigit())
Text("remaining")
.font(.caption)
}
}
DynamicIslandExpandedRegion(.center) {
// 可选的中心内容
}
DynamicIslandExpandedRegion(.bottom) {
HStack {
Button(intent: PauseIntent()) {
Label("Pause", systemImage: "pause.fill")
}
Button(intent: StopIntent()) {
Label("Stop", systemImage: "stop.fill")
}
}
}
}Design Principles (From WWDC 2023-10194)
设计原则(来自WWDC 2023-10194)
Concentric Alignment
同心对齐
"A key aspect to making things fit nicely inside the Dynamic Island is for them to be concentric with its shape. This is when rounded shapes nest inside of each other with even margins all the way around."
Visual mass (centroid) should nestle inside the Dynamic Island walls:
swift
// ✅ GOOD: Concentric circular shape
Circle()
.fill(.blue)
.frame(width: 44, height: 44)
// ❌ BAD: Square poking into corners
Rectangle()
.fill(.blue)
.frame(width: 44, height: 44)
// ✅ BETTER: Rounded rectangle
RoundedRectangle(cornerRadius: 12)
.fill(.blue)
.frame(width: 44, height: 44)"让内容在Dynamic Island中适配良好的关键是使其与Island的形状同心。即圆角形状相互嵌套,四周保持均匀边距。"
**视觉重心(质心)**应嵌入Dynamic Island的边框内:
swift
// ✅ 推荐:同心圆形
Circle()
.fill(.blue)
.frame(width: 44, height: 44)
// ❌ 不推荐:方形会突出到角落
Rectangle()
.fill(.blue)
.frame(width: 44, height: 44)
// ✅ 更好:圆角矩形
RoundedRectangle(cornerRadius: 12)
.fill(.blue)
.frame(width: 44, height: 44)Biological Motion
生物运动
Dynamic Island animations should feel organic and elastic, not mechanical:
swift
// Elastic spring animation
.animation(.spring(response: 0.6, dampingFraction: 0.7), value: isExpanded)
// Biological curve
.animation(.interpolatingSpring(stiffness: 300, damping: 25), value: content)Dynamic Island的动画应感觉自然且有弹性,而非机械感:
swift
// 弹性弹簧动画
.animation(.spring(response: 0.6, dampingFraction: 0.7), value: isExpanded)
// 生物曲线
.animation(.interpolatingSpring(stiffness: 300, damping: 25), value: content)Part 6: Control Center Widgets (iOS 18+)
第六部分:控制中心小组件(iOS 18+)
ControlWidget Protocol
ControlWidget协议
Controls appear in Control Center, Lock Screen, and Action Button (iPhone 15 Pro+).
控件会出现在控制中心、锁屏与动作按钮(iPhone 15 Pro+)中。
StaticControlConfiguration
StaticControlConfiguration
For simple controls without configuration.
swift
import WidgetKit
import AppIntents
struct TorchControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "TorchControl") {
ControlWidgetButton(action: ToggleTorchIntent()) {
Label("Flashlight", systemImage: "flashlight.on.fill")
}
}
.displayName("Flashlight")
.description("Toggle flashlight")
}
}适用于无需配置的简单控件。
swift
import WidgetKit
import AppIntents
struct TorchControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "TorchControl") {
ControlWidgetButton(action: ToggleTorchIntent()) {
Label("Flashlight", systemImage: "flashlight.on.fill")
}
}
.displayName("Flashlight")
.description("Toggle flashlight")
}
}AppIntentControlConfiguration
AppIntentControlConfiguration
For configurable controls.
swift
struct TimerControl: ControlWidget {
var body: some ControlWidgetConfiguration {
AppIntentControlConfiguration(
kind: "TimerControl",
intent: ConfigureTimerIntent.self
) { configuration in
ControlWidgetButton(action: StartTimerIntent(duration: configuration.duration)) {
Label("\(configuration.duration)m Timer", systemImage: "timer")
}
}
}
}适用于可配置的控件。
swift
struct TimerControl: ControlWidget {
var body: some ControlWidgetConfiguration {
AppIntentControlConfiguration(
kind: "TimerControl",
intent: ConfigureTimerIntent.self
) { configuration in
ControlWidgetButton(action: StartTimerIntent(duration: configuration.duration)) {
Label("\(configuration.duration)m Timer", systemImage: "timer")
}
}
}
}ControlWidgetButton
ControlWidgetButton
For discrete actions (one-shot operations).
swift
ControlWidgetButton(action: PlayMusicIntent()) {
Label("Play", systemImage: "play.fill")
}
.tint(.purple)用于离散操作(一次性操作)。
swift
ControlWidgetButton(action: PlayMusicIntent()) {
Label("Play", systemImage: "play.fill")
}
.tint(.purple)ControlWidgetToggle
ControlWidgetToggle
For boolean state.
swift
struct AirplaneModeControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "AirplaneModeControl") {
ControlWidgetToggle(
isOn: AirplaneModeIntent.isEnabled,
action: AirplaneModeIntent()
) { isOn in
Label(isOn ? "On" : "Off", systemImage: "airplane")
}
}
}
}用于布尔状态。
swift
struct AirplaneModeControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "AirplaneModeControl") {
ControlWidgetToggle(
isOn: AirplaneModeIntent.isEnabled,
action: AirplaneModeIntent()
) { isOn in
Label(isOn ? "On" : "Off", systemImage: "airplane")
}
}
}
}Value Providers (Async State)
值提供者(异步状态)
For controls that need to fetch current state asynchronously.
swift
struct TemperatureControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "ThermostatControl", provider: ThermostatProvider()) { value in
ControlWidgetButton(action: AdjustTemperatureIntent()) {
Label("\(value.temperature)°", systemImage: "thermometer")
}
}
}
}
struct ThermostatProvider: ControlValueProvider {
func currentValue() async throws -> ThermostatValue {
// Fetch current temperature from HomeKit/server
let temp = try await HomeManager.shared.currentTemperature()
return ThermostatValue(temperature: temp)
}
var previewValue: ThermostatValue {
ThermostatValue(temperature: 72) // Fallback for preview
}
}
struct ThermostatValue: ControlValueProviderValue {
var temperature: Int
}用于需要异步获取当前状态的控件。
swift
struct TemperatureControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "ThermostatControl", provider: ThermostatProvider()) { value in
ControlWidgetButton(action: AdjustTemperatureIntent()) {
Label("\(value.temperature)°", systemImage: "thermometer")
}
}
}
}
struct ThermostatProvider: ControlValueProvider {
func currentValue() async throws -> ThermostatValue {
// 从HomeKit/服务器获取当前温度
let temp = try await HomeManager.shared.currentTemperature()
return ThermostatValue(temperature: temp)
}
var previewValue: ThermostatValue {
ThermostatValue(temperature: 72) // 预览时的回退值
}
}
struct ThermostatValue: ControlValueProviderValue {
var temperature: Int
}Configurable Controls
可配置控件
Allow users to customize the control before adding.
swift
struct ConfigureTimerIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Configure Timer"
@Parameter(title: "Duration (minutes)", default: 5)
var duration: Int
}
struct TimerControl: ControlWidget {
var body: some ControlWidgetConfiguration {
AppIntentControlConfiguration(
kind: "TimerControl",
intent: ConfigureTimerIntent.self
) { config in
ControlWidgetButton(action: StartTimerIntent(duration: config.duration)) {
Label("\(config.duration)m", systemImage: "timer")
}
}
.promptsForUserConfiguration() // Show configuration UI when adding
}
}允许用户在添加前自定义控件。
swift
struct ConfigureTimerIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Configure Timer"
@Parameter(title: "Duration (minutes)", default: 5)
var duration: Int
}
struct TimerControl: ControlWidget {
var body: some ControlWidgetConfiguration {
AppIntentControlConfiguration(
kind: "TimerControl",
intent: ConfigureTimerIntent.self
) { config in
ControlWidgetButton(action: StartTimerIntent(duration: config.duration)) {
Label("\(config.duration)m", systemImage: "timer")
}
}
.promptsForUserConfiguration() // 添加时显示配置UI
}
}Control Refinements
控件优化
controlWidgetActionHint
controlWidgetActionHint
Accessibility hint for VoiceOver.
swift
ControlWidgetButton(action: ToggleTorchIntent()) {
Label("Flashlight", systemImage: "flashlight.on.fill")
}
.controlWidgetActionHint("Toggles flashlight")为VoiceOver提供无障碍提示。
swift
ControlWidgetButton(action: ToggleTorchIntent()) {
Label("Flashlight", systemImage: "flashlight.on.fill")
}
.controlWidgetActionHint("Toggles flashlight")displayName and description
displayName与description
swift
StaticControlConfiguration(kind: "MyControl") {
// ...
}
.displayName("My Control")
.description("Brief description shown in Control Center")swift
StaticControlConfiguration(kind: "MyControl") {
// ...
}
.displayName("My Control")
.description("Brief description shown in Control Center")Part 7: iOS 18+ Updates
第七部分:iOS 18+更新
Liquid Glass / Accented Rendering
液态玻璃/强调渲染
Widgets can render with accented glass effects matching system aesthetics (iOS 18+).
小组件可以使用强调玻璃效果渲染,匹配系统美学(iOS 18+)。
widgetAccentedRenderingMode
widgetAccentedRenderingMode
swift
struct MyWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "MyWidget", provider: Provider()) { entry in
MyWidgetView(entry: entry)
.widgetAccentedRenderingMode(.accented)
}
}
}swift
struct MyWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "MyWidget", provider: Provider()) { entry in
MyWidgetView(entry: entry)
.widgetAccentedRenderingMode(.accented)
}
}
}Rendering Modes
渲染模式
| Mode | Effect |
|---|---|
| System applies glass effect, respects vibrancy |
| Full color rendering (default) |
Design consideration: When , your widget's colors blend with system glass. Test in multiple contexts (Home Screen, StandBy, Lock Screen).
.accented| 模式 | 效果 |
|---|---|
| 系统应用玻璃效果,遵循活力效果 |
| 全彩色渲染(默认) |
设计注意事项:使用时,小组件的颜色会与系统玻璃融合。请在多个场景(主屏幕、StandBy、锁屏)中测试。
.accentedvisionOS Support
visionOS支持
Widgets supported on visionOS 2+ with spatial presentation.
visionOS 2+支持小组件,提供空间展示。
Mounting Styles
挂载样式
swift
#if os(visionOS)
struct MyWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "MyWidget", provider: Provider()) { entry in
MyWidgetView(entry: entry)
}
.supportedFamilies([.systemSmall, .systemMedium])
.ornamentLevel(.default) // Spatial ornament positioning
}
}
#endifswift
#if os(visionOS)
struct MyWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "MyWidget", provider: Provider()) { entry in
MyWidgetView(entry: entry)
}
.supportedFamilies([.systemSmall, .systemMedium])
.ornamentLevel(.default) // 空间装饰定位
}
}
#endifCarPlay Widgets (iOS 18+)
CarPlay小组件(iOS 18+)
Live Activities appear on CarPlay displays in supported vehicles.
swift
struct MyLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: NavigationAttributes.self) { context in
NavigationView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// Dynamic Island presentation
}
}
.supplementalActivityFamilies([
.small, // watchOS
.medium // CarPlay
])
}
}CarPlay rendering: Uses StandBy-style full-width presentation on the dashboard.
Live Activities会在支持的车辆的CarPlay显示屏上展示。
swift
struct MyLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: NavigationAttributes.self) { context in
NavigationView(context: context)
} dynamicIsland: { context in
DynamicIsland { /* ... */ }
}
.supplementalActivityFamilies([
.small, // watchOS
.medium // CarPlay
])
}
}CarPlay渲染:在仪表板上使用StandBy风格的全宽展示。
macOS Menu Bar
macOS菜单栏
Live Activities from paired iPhone appear in macOS menu bar automatically (no code changes required, macOS Sequoia+).
Presentation: Compact view appears in menu bar; clicking expands to show full content.
配对iPhone的Live Activities会自动出现在macOS菜单栏中(无需代码修改,macOS Sequoia+)。
展示方式:紧凑视图出现在菜单栏中;点击会展开显示完整内容。
watchOS Controls
watchOS控件
Control Center widgets available on watchOS 11+ in:
- Control Center
- Action Button
- Smart Stack (automatic suggestions)
swift
struct WatchControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "WatchControl") {
ControlWidgetButton(action: StartWorkoutIntent()) {
Label("Workout", systemImage: "figure.run")
}
}
}
}控制中心小组件在watchOS 11+中可用,支持:
- 控制中心
- 动作按钮
- 智能叠放(自动推荐)
swift
struct WatchControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "WatchControl") {
ControlWidgetButton(action: StartWorkoutIntent()) {
Label("Workout", systemImage: "figure.run")
}
}
}
}Relevance Widgets (iOS 18+)
相关性小组件(iOS 18+)
System intelligently promotes relevant widgets to Smart Stack on watchOS.
系统会智能地将相关小组件推送到watchOS的智能叠放中。
RelevanceConfiguration
RelevanceConfiguration
swift
struct RelevantWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "RelevantWidget", provider: Provider()) { entry in
RelevantWidgetView(entry: entry)
}
.relevanceConfiguration(
for: entry,
score: entry.relevanceScore,
attributes: [
.location(entry.userLocation),
.timeOfDay(entry.relevantTimeRange)
]
)
}
}swift
struct RelevantWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "RelevantWidget", provider: Provider()) { entry in
RelevantWidgetView(entry: entry)
}
.relevanceConfiguration(
for: entry,
score: entry.relevanceScore,
attributes: [
.location(entry.userLocation),
.timeOfDay(entry.relevantTimeRange)
]
)
}
}WidgetRelevanceAttribute
WidgetRelevanceAttribute
swift
enum WidgetRelevanceAttribute {
case location(CLLocation)
case timeOfDay(DateInterval)
case activity(String) // Calendar event, workout, etc.
}swift
enum WidgetRelevanceAttribute {
case location(CLLocation)
case timeOfDay(DateInterval)
case activity(String) // 日历事件、锻炼等
}Push Notification Updates (iOS 18+)
推送通知更新(iOS 18+)
WidgetPushHandler
WidgetPushHandler
Server-to-widget push notifications with cross-device sync.
swift
class WidgetPushHandler: NSObject, PKPushRegistryDelegate {
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
if type == .widgetKit {
// Update widget data in shared container
let shared = UserDefaults(suiteName: "group.com.myapp")!
if let data = payload.dictionaryPayload["widgetData"] as? [String: Any] {
shared.set(data, forKey: "widgetData")
}
// Reload widgets
WidgetCenter.shared.reloadAllTimelines()
}
}
}Cross-device sync: Push to iPhone automatically syncs to Apple Watch and CarPlay Live Activities.
服务器到小组件的推送通知,支持跨设备同步。
swift
class WidgetPushHandler: NSObject, PKPushRegistryDelegate {
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
if type == .widgetKit {
// 更新共享容器中的小组件数据
let shared = UserDefaults(suiteName: "group.com.myapp")!
if let data = payload.dictionaryPayload["widgetData"] as? [String: Any] {
shared.set(data, forKey: "widgetData")
}
// 重载小组件
WidgetCenter.shared.reloadAllTimelines()
}
}
}跨设备同步:推送到iPhone的通知会自动同步到Apple Watch和CarPlay的Live Activities。
Part 8: App Groups & Data Sharing
第八部分:App Groups与数据共享
App Groups Entitlement
App Groups权限
Required for sharing data between your app and extensions.
在应用与扩展之间共享数据是必需的。
Configuration
配置
- Xcode: Targets → Signing & Capabilities → Add "App Groups"
- Identifier format:
group.com.company.appname - Enable for both: Main app target AND extension target
- Xcode:Targets → Signing & Capabilities → 添加"App Groups"
- 标识符格式:
group.com.company.appname - 同时启用:主应用目标与扩展目标都要启用
Example Entitlement File
示例权限文件
xml
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.mycompany.myapp</string>
</array>xml
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.mycompany.myapp</string>
</array>Shared Containers
共享容器
Access Shared Container
访问共享容器
swift
let sharedContainer = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.mycompany.myapp"
)!
let dataFileURL = sharedContainer.appendingPathComponent("widgetData.json")swift
let sharedContainer = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.mycompany.myapp"
)!
let dataFileURL = sharedContainer.appendingPathComponent("widgetData.json")UserDefaults with App Groups
搭配App Groups的UserDefaults
swift
// Main app - write data
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
shared.set("Updated value", forKey: "myKey")
// Widget extension - read data
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
let value = shared.string(forKey: "myKey")swift
// 主应用——写入数据
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
shared.set("Updated value", forKey: "myKey")
// 小组件扩展——读取数据
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
let value = shared.string(forKey: "myKey")Core Data with App Groups
搭配App Groups的Core Data
swift
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MyApp")
let sharedStoreURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.mycompany.myapp"
)!.appendingPathComponent("MyApp.sqlite")
let description = NSPersistentStoreDescription(url: sharedStoreURL)
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { description, error in
// Handle errors
}
return container
}()swift
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MyApp")
let sharedStoreURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.mycompany.myapp"
)!.appendingPathComponent("MyApp.sqlite")
let description = NSPersistentStoreDescription(url: sharedStoreURL)
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { description, error in
// 处理错误
}
return container
}()IPC Communication
IPC通信
Background URL Session (For Downloads)
后台URL会话(用于下载)
swift
// Main app
let config = URLSessionConfiguration.background(withIdentifier: "com.mycompany.myapp.background")
config.sharedContainerIdentifier = "group.com.mycompany.myapp"
let session = URLSession(configuration: config)swift
// 主应用
let config = URLSessionConfiguration.background(withIdentifier: "com.mycompany.myapp.background")
config.sharedContainerIdentifier = "group.com.mycompany.myapp"
let session = URLSession(configuration: config)Darwin Notification Center (Simple Signals)
Darwin通知中心(简单信号)
swift
import Foundation
// Post notification
CFNotificationCenterPostNotification(
CFNotificationCenterGetDarwinNotifyCenter(),
CFNotificationName("com.mycompany.myapp.dataUpdated" as CFString),
nil, nil, true
)
// Observe notification (in widget)
CFNotificationCenterAddObserver(
CFNotificationCenterGetDarwinNotifyCenter(),
Unmanaged.passUnretained(self).toOpaque(),
{ (center, observer, name, object, userInfo) in
// Reload widget
WidgetCenter.shared.reloadAllTimelines()
},
"com.mycompany.myapp.dataUpdated" as CFString,
nil, .deliverImmediately
)swift
import Foundation
// 发送通知
CFNotificationCenterPostNotification(
CFNotificationCenterGetDarwinNotifyCenter(),
CFNotificationName("com.mycompany.myapp.dataUpdated" as CFString),
nil, nil, true
)
// 监听通知(在小组件中)
CFNotificationCenterAddObserver(
CFNotificationCenterGetDarwinNotifyCenter(),
Unmanaged.passUnretained(self).toOpaque(),
{ (center, observer, name, object, userInfo) in
// 重载小组件
WidgetCenter.shared.reloadAllTimelines()
},
"com.mycompany.myapp.dataUpdated" as CFString,
nil, .deliverImmediately
)Part 9: watchOS Integration
第九部分:watchOS集成
supplementalActivityFamilies (watchOS 11+)
supplementalActivityFamilies(watchOS 11+)
Live Activities from iPhone automatically appear on Apple Watch Smart Stack.
swift
struct MyLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// iPhone presentation
DeliveryView(context: context)
} dynamicIsland: { context in
// Dynamic Island (iPhone only)
DynamicIsland { /* ... */ }
}
.supplementalActivityFamilies([.small]) // Enable watchOS
}
}iPhone的Live Activities会自动出现在Apple Watch智能叠放中。
swift
struct MyLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// iPhone展示
DeliveryView(context: context)
} dynamicIsland: { context in
// Dynamic Island(仅iPhone)
DynamicIsland { /* ... */ }
}
.supplementalActivityFamilies([.small]) // 启用watchOS支持
}
}activityFamily Environment
activityFamily环境
Adapt layout for Apple Watch.
swift
struct DeliveryView: View {
@Environment(\.activityFamily) var activityFamily
var context: ActivityViewContext<DeliveryAttributes>
var body: some View {
if activityFamily == .small {
// watchOS-optimized layout
WatchDeliveryView(context: context)
} else {
// iPhone layout
iPhoneDeliveryView(context: context)
}
}
}为Apple Watch适配布局。
swift
struct DeliveryView: View {
@Environment(\.activityFamily) var activityFamily
var context: ActivityViewContext<DeliveryAttributes>
var body: some View {
if activityFamily == .small {
// 优化的watchOS布局
WatchDeliveryView(context: context)
} else {
// iPhone布局
iPhoneDeliveryView(context: context)
}
}
}Always On Display Adaptation
始终显示适配
isLuminanceReduced
isLuminanceReduced
swift
struct WatchWidgetView: View {
@Environment(\.isLuminanceReduced) var isLuminanceReduced
var body: some View {
if isLuminanceReduced {
// Simplified view for Always On Display
Text(timeString)
.font(.system(.title, design: .rounded))
} else {
// Full color, detailed view
VStack {
Text(timeString).font(.title)
Text(statusString).font(.caption)
}
}
}
}swift
struct WatchWidgetView: View {
@Environment(\.isLuminanceReduced) var isLuminanceReduced
var body: some View {
if isLuminanceReduced {
// 始终显示模式的简化视图
Text(timeString)
.font(.system(.title, design: .rounded))
} else {
// 全彩详细视图
VStack {
Text(timeString).font(.title)
Text(statusString).font(.caption)
}
}
}
}Color Scheme Adaptation
更新预算(watchOS)
swift
@Environment(\.colorScheme) var colorScheme
var body: some View {
Text("Status")
.foregroundColor(
isLuminanceReduced
? .white // Always On: white text
: (colorScheme == .dark ? .white : .black)
)
}同步:watchOS Live Activity的更新与iPhone同步。当iPhone通过推送通知接收更新时,watchOS会自动刷新。
连接性:如果Apple Watch超出范围或蓝牙断开,更新可能会延迟。
Update Budgeting (watchOS)
第十部分:实用工作流
—
构建你的第一个小组件
Synchronization: watchOS Live Activity updates are synchronized with iPhone. When iPhone receives an update via push notification, watchOS automatically refreshes.
Connectivity: Updates may be delayed if Apple Watch is out of range or Bluetooth is disconnected.
如需完整的分步教程及可用代码示例,请查看苹果的使用WidgetKit和SwiftUI构建小组件示例项目。
关键步骤:添加小组件扩展目标、配置App Groups、实现TimelineProvider、设计SwiftUI视图、从主应用更新。生产环境要求请见下方的专家审核清单。
Part 10: Practical Workflows
专家审核清单
Building Your First Widget
发布小组件前
For a complete step-by-step tutorial with working code examples, see Apple's Building Widgets Using WidgetKit and SwiftUI sample project.
Key steps: Add widget extension target, configure App Groups, implement TimelineProvider, design SwiftUI view, update from main app. See Expert Review Checklist below for production requirements.
架构:
- App Groups权限已在应用和扩展中配置
- 两个目标中的组标识符完全匹配
- 所有数据共享都使用共享容器
- 小组件代码中未使用
UserDefaults.standard
性能:
- 时间线生成在5秒内完成
- 小组件视图中无网络请求
- 时间线有合理的刷新间隔(≥15分钟)
- 条目数量合理(<20-30个)
- 内存使用在限制内(小组件约30MB,活动约50MB)
- 图片已优化(优先使用资源目录或SF Symbols)
数据与状态:
- 小组件能优雅处理缺失/空数据
- 条目日期按时间顺序排列
- 占位符视图显示合理
- 快照视图能代表实际使用场景
用户体验:
- 小组件出现在小组件图库中
- configurationDisplayName清晰简洁
- description解释了小组件的用途
- 所有支持的类型都已测试且显示正确
- 文字在亮色和暗色背景上都可读
- 交互元素(按钮/开关)工作正常
Live Activities(如适用):
- ActivityAttributes大小小于4KB
- 启动前已检查授权
- 事件完成时活动已结束
- 设置了正确的关闭策略
- 已配置watchOS支持(如相关,supplementalActivityFamilies)
- 已测试Dynamic Island布局(紧凑、最小化、展开)
控制中心小组件(如适用):
- ControlValueProvider异步且快速(<1秒)
- previewValue提供了合理的回退值
- 已设置displayName和description
- 已在控制中心、锁屏、动作按钮中测试
测试:
- 在实际设备上测试(不仅是模拟器)
- 测试了添加/移除小组件
- 测试了应用数据变化→小组件更新
- 测试了强制退出应用→小组件仍能工作
- 测试了低内存场景
- 测试了所有支持的iOS版本
- 测试了无网络连接的情况
Expert Review Checklist
测试指南
Before Shipping Widgets
时间线提供者的单元测试
Architecture:
- App Groups entitlement configured in app AND extension
- Group identifier matches exactly in both targets
- Shared container used for ALL data sharing
- No in widget code
UserDefaults.standard
Performance:
- Timeline generation completes in < 5 seconds
- No network requests in widget views
- Timeline has reasonable refresh intervals (≥ 15 min)
- Entry count reasonable (< 20-30 entries)
- Memory usage under limits (~30MB widgets, ~50MB activities)
- Images optimized (asset catalog or SF Symbols preferred)
Data & State:
- Widget handles missing/nil data gracefully
- Entry dates in chronological order
- Placeholder view looks reasonable
- Snapshot view representative of actual use
User Experience:
- Widget appears in widget gallery
- configurationDisplayName clear and concise
- description explains widget purpose
- All supported families tested and look correct
- Text readable on both light and dark backgrounds
- Interactive elements (buttons/toggles) work correctly
Live Activities (if applicable):
- ActivityAttributes under 4KB
- Authorization checked before starting
- Activity ends when event completes
- Proper dismissal policy set
- watchOS support configured if relevant (supplementalActivityFamilies)
- Dynamic Island layouts tested (compact, minimal, expanded)
Control Center Widgets (if applicable):
- ControlValueProvider async and fast (< 1 second)
- previewValue provides reasonable fallback
- displayName and description set
- Tested in Control Center, Lock Screen, Action Button
Testing:
- Tested on actual device (not just simulator)
- Tested adding/removing widget
- Tested app data changes → widget updates
- Tested force-quit app → widget still works
- Tested low memory scenarios
- Tested all iOS versions you support
- Tested with no internet connection
swift
import XCTest
import WidgetKit
@testable import MyWidgetExtension
class TimelineProviderTests: XCTestCase {
var provider: Provider!
override func setUp() {
super.setUp()
provider = Provider()
}
func testPlaceholderReturnsValidEntry() {
let context = MockContext()
let entry = provider.placeholder(in: context)
XCTAssertNotNil(entry)
// 占位符应包含默认/安全值
}
func testTimelineGenerationWithValidData() {
// 设置:将测试数据保存到共享容器
let testData = WidgetData(title: "Test", value: 100, lastUpdated: Date())
SharedDataManager.shared.saveData(testData)
let expectation = expectation(description: "Timeline generated")
let context = MockContext()
provider.getTimeline(in: context) { timeline in
XCTAssertFalse(timeline.entries.isEmpty)
XCTAssertEqual(timeline.entries.first?.widgetData?.title, "Test")
expectation.fulfill()
}
waitForExpectations(timeout: 5.0)
}
}Testing Guidance
手动测试清单
Unit Testing Timeline Providers
—
swift
import XCTest
import WidgetKit
@testable import MyWidgetExtension
class TimelineProviderTests: XCTestCase {
var provider: Provider!
override func setUp() {
super.setUp()
provider = Provider()
}
func testPlaceholderReturnsValidEntry() {
let context = MockContext()
let entry = provider.placeholder(in: context)
XCTAssertNotNil(entry)
// Placeholder should have default/safe values
}
func testTimelineGenerationWithValidData() {
// Setup: Save test data to shared container
let testData = WidgetData(title: "Test", value: 100, lastUpdated: Date())
SharedDataManager.shared.saveData(testData)
let expectation = expectation(description: "Timeline generated")
let context = MockContext()
provider.getTimeline(in: context) { timeline in
XCTAssertFalse(timeline.entries.isEmpty)
XCTAssertEqual(timeline.entries.first?.widgetData?.title, "Test")
expectation.fulfill()
}
waitForExpectations(timeout: 5.0)
}
}基本功能:
- 将小组件添加到主屏幕
- 验证它出现在小组件图库中
- 检查所有支持的尺寸显示正确
- 确认数据与应用数据匹配
数据更新:
- 在主应用中修改数据
- 观察小组件更新(可能需要几秒)
- 强制退出应用,验证小组件仍能显示数据
- 重启设备,验证小组件仍存在
边缘情况:
- 删除所有应用数据,验证小组件能优雅处理
- 禁用网络,验证小组件能离线工作
- 启用低电量模式,验证小组件遵守限制
- 添加多个相同小组件的实例
性能:
- 在Xcode中监控内存使用(调试导航器)
- 在控制台日志中检查时间线生成时间
- 验证崩溃日志中无崩溃记录
- 在旧设备上测试(不仅是最新iPhone)
Manual Testing Checklist
调试技巧
Basic Functionality:
- Add widget to Home Screen
- Verify it shows in widget gallery
- Check all supported sizes display correctly
- Confirm data matches app data
Data Updates:
- Change data in main app
- Observe widget updates (may take seconds)
- Force-quit app, verify widget still shows data
- Reboot device, verify widget persists
Edge Cases:
- Delete all app data, verify widget handles gracefully
- Disable network, verify widget works offline
- Enable Low Power Mode, verify widget respects limits
- Add multiple instances of same widget
Performance:
- Monitor memory usage in Xcode (Debug Navigator)
- Check timeline generation time in Console logs
- Verify no crashes in crash logs
- Test on older devices (not just latest iPhone)
小组件不更新?
swift
// 在getTimeline()中添加日志
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
print("⏰ Widget timeline requested at \(Date())")
let data = SharedDataManager.shared.loadData()
print("📊 Loaded data: \(String(describing: data))")
// ...
}
// 数据变化时从主应用手动重载
import WidgetKit
print("🔄 Reloading widget timelines")
WidgetCenter.shared.reloadAllTimelines()检查控制台日志:
Widget: ⏰ Widget timeline requested at 2024-01-15 10:30:00
Widget: 📊 Loaded data: Optional(WidgetData(title: "Test", value: 42))验证App Groups:
swift
// 在应用和小组件中,验证路径相同
let container = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.yourcompany.yourapp"
)
print("📁 Container path: \(container?.path ?? "nil")")
// 两者应打印相同的路径Debugging Tips
第十部分:故障排除
—
小组件未出现在图库中
Widget not updating?
swift
// Add logging to getTimeline()
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
print("⏰ Widget timeline requested at \(Date())")
let data = SharedDataManager.shared.loadData()
print("📊 Loaded data: \(String(describing: data))")
// ...
}
// In main app after data change
print("🔄 Reloading widget timelines")
WidgetCenter.shared.reloadAllTimelines()Check Console logs:
Widget: ⏰ Widget timeline requested at 2024-01-15 10:30:00
Widget: 📊 Loaded data: Optional(WidgetData(title: "Test", value: 42))Verify App Groups:
swift
// In both app and widget, verify same path
let container = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.yourcompany.yourapp"
)
print("📁 Container path: \(container?.path ?? "nil")")
// Both should print SAME path症状:小组件未出现在小组件选择器中
诊断步骤:
- 检查是否包含你的小组件
WidgetBundle - 验证已设置
supportedFamilies() - 检查扩展目标的"Skip Install"是否为NO
- 验证扩展的部署目标与应用匹配
解决方案:
swift
@main
struct MyWidgetBundle: WidgetBundle {
var body: some Widget {
MyWidget()
// 如果缺失,在此添加你的小组件
}
}Part 11: Troubleshooting
小组件不刷新
Widget Not Appearing in Gallery
—
Symptoms: Widget doesn't show up in the widget picker
Diagnostic Steps:
- Check includes your widget
WidgetBundle - Verify is set
supportedFamilies() - Check extension target's "Skip Install" is NO
- Verify extension's deployment target matches app
Solution:
swift
@main
struct MyWidgetBundle: WidgetBundle {
var body: some Widget {
MyWidget()
// Add your widget here if missing
}
}症状:小组件显示过期数据,不更新
诊断步骤:
- 检查时间线策略(vs
.atEndvs.after()).never - 验证未超过每日预算(40-70次重载)
- 检查是否被调用(添加日志)
getTimeline() - 确保App Groups已正确配置以共享数据
解决方案:
swift
// 数据变化时从主应用手动重载
import WidgetKit
WidgetCenter.shared.reloadAllTimelines()
// 或
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")Widget Not Refreshing
应用与小组件之间数据未共享
Symptoms: Widget shows stale data, doesn't update
Diagnostic Steps:
- Check timeline policy (vs
.atEndvs.after()).never - Verify you're not exceeding daily budget (40-70 reloads)
- Check if is being called (add logging)
getTimeline() - Ensure App Groups configured correctly for shared data
Solution:
swift
// Manual reload from main app when data changes
import WidgetKit
WidgetCenter.shared.reloadAllTimelines()
// or
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")症状:小组件显示默认/空数据
诊断步骤:
- 验证App Groups权限已在两个目标中配置
- 检查组标识符完全匹配
- 确保两个目标使用相同的suiteName
- 如果使用共享容器,检查文件路径
解决方案:
swift
// 应用和扩展都必须使用:
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
// 不要使用:
let shared = UserDefaults.standard // ❌ 不同的容器Data Not Shared Between App and Widget
Live Activity无法启动
Symptoms: Widget shows default/empty data
Diagnostic Steps:
- Verify App Groups entitlement in BOTH targets
- Check group identifier matches exactly
- Ensure using same suiteName in both targets
- Check file path if using shared container
Solution:
swift
// Both app AND extension must use:
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
// NOT:
let shared = UserDefaults.standard // ❌ Different containers症状:抛出错误
Activity.request()常见错误:
"Activity size exceeds 4KB":
swift
// ❌ 不推荐:属性中包含大图片
struct MyAttributes: ActivityAttributes {
var productImage: UIImage // 太大!
}
// ✅ 推荐:使用资源目录名称
struct MyAttributes: ActivityAttributes {
var productImageName: String // 引用资源
}"Activities not enabled":
swift
// 先检查授权
let authInfo = ActivityAuthorizationInfo()
guard authInfo.areActivitiesEnabled else {
throw ActivityError.notEnabled
}Live Activity Won't Start
交互式小组件按钮不工作
Symptoms: throws error
Activity.request()Common Errors:
"Activity size exceeds 4KB":
swift
// ❌ BAD: Large images in attributes
struct MyAttributes: ActivityAttributes {
var productImage: UIImage // Too large!
}
// ✅ GOOD: Use asset catalog names
struct MyAttributes: ActivityAttributes {
var productImageName: String // Reference to asset
}"Activities not enabled":
swift
// Check authorization first
let authInfo = ActivityAuthorizationInfo()
guard authInfo.areActivitiesEnabled else {
throw ActivityError.notEnabled
}症状:点击按钮无反应
诊断步骤:
- 验证App Intent的返回
perform()IntentResult - 检查小组件目标中已导入intent
- 确保按钮使用参数,而非
intent:action: - 检查控制台中的intent执行错误
解决方案:
swift
// ✅ 正确:使用intent参数
Button(intent: MyIntent()) {
Label("Action", systemImage: "star")
}
// ❌ 错误:不要使用action闭包
Button(action: { /* 这在小组件中无效 */ }) {
Label("Action", systemImage: "star")
}Interactive Widget Button Not Working
控制中心小组件缓慢/无响应
Symptoms: Tapping button does nothing
Diagnostic Steps:
- Verify App Intent's returns
perform()IntentResult - Check intent is imported in widget target
- Ensure button uses parameter, not
intent:action: - Check Console for intent execution errors
Solution:
swift
// ✅ CORRECT: Use intent parameter
Button(intent: MyIntent()) {
Label("Action", systemImage: "star")
}
// ❌ WRONG: Don't use action closure
Button(action: { /* This won't work in widgets */ }) {
Label("Action", systemImage: "star")
}症状:控件需要几秒响应,看起来冻结
原因:或intent 中执行了同步操作
ControlValueProviderperform()解决方案:
swift
struct MyValueProvider: ControlValueProvider {
func currentValue() async throws -> MyValue {
// ✅ 推荐:异步获取
let value = try await fetchCurrentValue()
return MyValue(data: value)
}
var previewValue: MyValue {
// ✅ 推荐:快速回退
MyValue(data: "Loading...")
}
}
// ❌ 不推荐:阻塞主线程
func currentValue() async throws -> MyValue {
Thread.sleep(forTimeInterval: 2.0) // 阻塞UI
}Control Center Widget Slow/Unresponsive
小组件显示错误的尺寸/布局
Symptoms: Control takes seconds to respond, appears frozen
Cause: Synchronous work in or intent
ControlValueProviderperform()Solution:
swift
struct MyValueProvider: ControlValueProvider {
func currentValue() async throws -> MyValue {
// ✅ GOOD: Async fetch
let value = try await fetchCurrentValue()
return MyValue(data: value)
}
var previewValue: MyValue {
// ✅ GOOD: Fast fallback
MyValue(data: "Loading...")
}
}
// ❌ BAD: Don't block main thread
func currentValue() async throws -> MyValue {
Thread.sleep(forTimeInterval: 2.0) // Blocks UI
}症状:小组件被裁剪或比例不正确
诊断步骤:
- 检查视图代码中的
entry.family - 验证视图已适配类型尺寸
- 测试所有支持的类型
- 检查是否有硬编码的尺寸
解决方案:
swift
struct MyWidgetView: View {
@Environment(\.widgetFamily) var family
var entry: Provider.Entry
var body: some View {
switch family {
case .systemSmall:
SmallLayout(entry: entry)
case .systemMedium:
MediumLayout(entry: entry)
default:
Text("Unsupported")
}
}
}Widget Shows Wrong Size/Layout
时间线条目未按顺序显示
Symptoms: Widget clipped or incorrect aspect ratio
Diagnostic Steps:
- Check in view code
entry.family - Verify view adapts to family size
- Test all supported families
- Check for hardcoded sizes
Solution:
swift
struct MyWidgetView: View {
@Environment(\.widgetFamily) var family
var entry: Provider.Entry
var body: some View {
switch family {
case .systemSmall:
SmallLayout(entry: entry)
case .systemMedium:
MediumLayout(entry: entry)
default:
Text("Unsupported")
}
}
}症状:小组件随机跳转到不同条目
原因:条目日期未按时间顺序排列
解决方案:
swift
// ✅ 推荐:按时间顺序的日期
let now = Date()
let entries = (0..<5).map { offset in
let date = Calendar.current.date(byAdding: .hour, value: offset, to: now)!
return SimpleEntry(date: date, data: "Entry \(offset)")
}
// ❌ 不推荐:日期顺序混乱
let entries = [
SimpleEntry(date: Date().addingTimeInterval(3600), data: "2"),
SimpleEntry(date: Date(), data: "1"), // 顺序混乱
]Timeline Entries Not Appearing in Order
watchOS Live Activity未显示
Symptoms: Widget jumps between entries randomly
Cause: Entry dates not in chronological order
Solution:
swift
// ✅ GOOD: Chronological dates
let now = Date()
let entries = (0..<5).map { offset in
let date = Calendar.current.date(byAdding: .hour, value: offset, to: now)!
return SimpleEntry(date: date, data: "Entry \(offset)")
}
// ❌ BAD: Out of order dates
let entries = [
SimpleEntry(date: Date().addingTimeInterval(3600), data: "2"),
SimpleEntry(date: Date(), data: "1"), // Out of order
]症状:活动在iPhone上显示,但未在Apple Watch上显示
诊断步骤:
- 检查已设置
.supplementalActivityFamilies([.small]) - 验证Apple Watch已配对且在附近
- 检查watchOS版本(11+)
- 确保蓝牙已启用
解决方案:
swift
ActivityConfiguration(for: MyAttributes.self) { context in
MyActivityView(context: context)
} dynamicIsland: { context in
DynamicIsland { /* ... */ }
}
.supplementalActivityFamilies([.small]) // watchOS支持必需watchOS Live Activity Not Showing
性能问题
Symptoms: Activity appears on iPhone but not Apple Watch
Diagnostic Steps:
- Check is set
.supplementalActivityFamilies([.small]) - Verify Apple Watch is paired and nearby
- Check watchOS version (11+)
- Ensure Bluetooth enabled
Solution:
swift
ActivityConfiguration(for: MyAttributes.self) { context in
MyActivityView(context: context)
} dynamicIsland: { context in
DynamicIsland { /* ... */ }
}
.supplementalActivityFamilies([.small]) // Required for watchOS症状:小组件渲染缓慢,电池消耗快
常见原因:
- 时间线条目过多(>100个)
- 视图代码中有网络请求
- 中有大量计算
getTimeline() - 刷新间隔过于频繁(<15分钟)
解决方案:
swift
// ✅ 推荐:合理的间隔
let entries = (0..<8).map { offset in
let date = Calendar.current.date(byAdding: .minute, value: offset * 15, to: now)!
return SimpleEntry(date: date, data: precomputedData)
}
// ❌ 不推荐:过于频繁,条目过多
let entries = (0..<100).map { offset in
let date = Calendar.current.date(byAdding: .minute, value: offset, to: now)!
return SimpleEntry(date: date, data: fetchFromNetwork()) // 时间线中发起网络请求
}Performance Issues
资源
Symptoms: Widget rendering slow, battery drain
Common Causes:
- Too many timeline entries (> 100)
- Network requests in view code
- Heavy computation in
getTimeline() - Refresh intervals too frequent (< 15 min)
Solution:
swift
// ✅ GOOD: Strategic intervals
let entries = (0..<8).map { offset in
let date = Calendar.current.date(byAdding: .minute, value: offset * 15, to: now)!
return SimpleEntry(date: date, data: precomputedData)
}
// ❌ BAD: Too frequent, too many entries
let entries = (0..<100).map { offset in
let date = Calendar.current.date(byAdding: .minute, value: offset, to: now)!
return SimpleEntry(date: date, data: fetchFromNetwork()) // Network in timeline
}WWDC:2025-278, 2024-10157, 2024-10068, 2024-10098, 2023-10028, 2023-10194, 2022-10184, 2022-10185
文档:/widgetkit, /activitykit, /appintents
技能:axiom-app-intents-ref, axiom-swift-concurrency, axiom-swiftui-performance, axiom-swiftui-layout, axiom-extensions-widgets
版本:0.9 | 平台:iOS 14+, iPadOS 14+, watchOS 9+, macOS 11+, axiom-visionOS 2+
Resources
—
WWDC: 2025-278, 2024-10157, 2024-10068, 2024-10098, 2023-10028, 2023-10194, 2022-10184, 2022-10185
Docs: /widgetkit, /activitykit, /appintents
Skills: axiom-app-intents-ref, axiom-swift-concurrency, axiom-swiftui-performance, axiom-swiftui-layout, axiom-extensions-widgets
Version: 0.9 | Platforms: iOS 14+, iPadOS 14+, watchOS 9+, macOS 11+, axiom-visionOS 2+
—