axiom-extensions-widgets-ref

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Extensions & 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
IntentConfiguration
with SiriKit intents. Migrate to
AppIntentConfiguration
for iOS 17+.
适用于使用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及更早版本使用
IntentConfiguration
搭配SiriKit intents,iOS 17+需迁移至
AppIntentConfiguration

ActivityConfiguration

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 projects
Configuration Type Comparison:
ConfigurationUse WhenExample
StaticConfigurationNo user customization neededWeather for current location, battery status
AppIntentConfiguration (simple)Fixed list of optionsTimer presets, theme selection
AppIntentConfiguration (EntityQuery)Dynamic list from app dataProject picker, contact picker, playlist selector
ActivityConfigurationLive ongoing eventsDelivery tracking, workout progress, sports scores
决策树
你的小组件是否需要用户配置?
├─ 否 → 使用StaticConfiguration
│  └─ 示例:当前位置的天气小组件
└─ 是 → 需要配置
   ├─ 简单静态选项(无动态数据)?
   │  └─ 使用AppIntentConfiguration搭配WidgetConfigurationIntent
   │     └─ 示例:带有预设时长(5、10、15分钟)的计时器
   └─ 动态选项(项目、联系人、播放列表)?
      └─ 使用AppIntentConfiguration + EntityQuery
         └─ 示例:展示用户项目的项目状态小组件
配置类型对比
配置类型使用场景示例
StaticConfiguration无需用户自定义当前位置天气、电池状态
AppIntentConfiguration(简单型)固定选项列表计时器预设、主题选择
AppIntentConfiguration(EntityQuery)来自应用数据的动态列表项目选择器、联系人选择器、播放列表选择器
ActivityConfiguration正在进行的实时事件配送跟踪、锻炼进度、体育比分

Widget Families

小组件类型

System Families (Home Screen)

系统类型(主屏幕)

FamilySize (points)iOS VersionUse Case
systemSmall
~170×17014+Single piece of info, icon
systemMedium
~360×17014+Multiple data points, chart
systemLarge
~360×38014+Detailed view, list
systemExtraLarge
~720×38015+ (iPad only)Rich layouts, multiple views
类型尺寸(点)iOS版本使用场景
systemSmall
~170×17014+单一信息、图标
systemMedium
~360×17014+多个数据点、图表
systemLarge
~360×38014+详细视图、列表
systemExtraLarge
~720×38015+(仅iPad)丰富布局、多视图

Accessory Families (Lock Screen, iOS 16+)

配件类型(锁屏,iOS 16+)

FamilyLocationSizeContent
accessoryCircular
Circular complication~48×48ptIcon or gauge
accessoryRectangular
Above clock~160×72ptText + icon
accessoryInline
Above dateSingle lineText only
类型位置尺寸内容
accessoryCircular
圆形复杂控件~48×48pt图标或仪表盘
accessoryRectangular
时钟上方~160×72pt文字+图标
accessoryInline
日期上方单行仅文字

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.
PolicyBehavior
.atEnd
Reload after last entry
.after(date)
Reload at specific date
.never
No automatic reload (manual only)
控制系统何时请求新时间线。
策略行为
.atEnd
最后一个条目展示后重载
.after(date)
在指定日期重载
.never
不自动重载(仅手动重载)

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
getTimeline()
in under 5 seconds
Strategies:
  1. Cache in main app - Precompute expensive operations
  2. Async/await - Don't block completion handler
  3. Limit entries - 10-20 entries maximum
  4. 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
}
目标
getTimeline()
的执行时间控制在5秒以内
策略
  1. 在主应用中缓存——预计算耗时操作
  2. 使用Async/await——不要阻塞完成处理程序
  3. 限制条目数量——最多10-20个条目
  4. 最小化计算——仅进行简单转换
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 StrategyDaily Budget UsedBattery Impact
Strategic (4x/hour)~48 reloadsLow
Aggressive (12x/hour)Budget exhausted by 6 PMHigh
On-demand only5-10 reloadsMinimal
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
TimelineEntry
, keep views simple.
小组件会频繁渲染(每次用户查看主屏幕/锁屏时)
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 { /* 耗时转换 */ }
    }
}
规则:所有内容都在
TimelineEntry
中预计算,保持视图简单。

Image 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 termination

swift
// ✅ 推荐:资源目录图片(快速)
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
Button
and
Toggle
with App Intents.
交互式小组件使用搭配App Intents的SwiftUI
Button
Toggle

Button 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
.invalidatableContent()
becomes slightly transparent while the associated intent executes, providing user feedback.
在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")
            }
        }
    }
}
效果:带有
.invalidatableContent()
的内容在关联Intent执行期间会略微透明,为用户提供反馈。

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:
ActivityAttributes
total data size must be under 4KB to start successfully.
定义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
}
关键限制
ActivityAttributes
的总数据大小必须小于4KB才能成功启动。

Starting Activities

启动活动

Request Authorization

请求授权

swift
import ActivityKit

let authorizationInfo = ActivityAuthorizationInfo()
let areActivitiesEnabled = authorizationInfo.areActivitiesEnabled
swift
import ActivityKit

let authorizationInfo = ActivityAuthorizationInfo()
let areActivitiesEnabled = authorizationInfo.areActivitiesEnabled

Start 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" entitlement
Standard 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

渲染模式

ModeEffect
.accented
System applies glass effect, respects vibrancy
.fullColor
Full color rendering (default)
Design consideration: When
.accented
, your widget's colors blend with system glass. Test in multiple contexts (Home Screen, StandBy, Lock Screen).
模式效果
.accented
系统应用玻璃效果,遵循活力效果
.fullColor
全彩色渲染(默认)
设计注意事项:使用
.accented
时,小组件的颜色会与系统玻璃融合。请在多个场景(主屏幕、StandBy、锁屏)中测试。

visionOS 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
    }
}
#endif
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) // 空间装饰定位
    }
}
#endif

CarPlay 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

配置

  1. Xcode: Targets → Signing & Capabilities → Add "App Groups"
  2. Identifier format:
    group.com.company.appname
  3. Enable for both: Main app target AND extension target
  1. Xcode:Targets → Signing & Capabilities → 添加"App Groups"
  2. 标识符格式
    group.com.company.appname
  3. 同时启用:主应用目标与扩展目标都要启用

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
    UserDefaults.standard
    in widget code
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)
    }
}
基本功能
  1. 将小组件添加到主屏幕
  2. 验证它出现在小组件图库中
  3. 检查所有支持的尺寸显示正确
  4. 确认数据与应用数据匹配
数据更新
  1. 在主应用中修改数据
  2. 观察小组件更新(可能需要几秒)
  3. 强制退出应用,验证小组件仍能显示数据
  4. 重启设备,验证小组件仍存在
边缘情况
  1. 删除所有应用数据,验证小组件能优雅处理
  2. 禁用网络,验证小组件能离线工作
  3. 启用低电量模式,验证小组件遵守限制
  4. 添加多个相同小组件的实例
性能
  1. 在Xcode中监控内存使用(调试导航器)
  2. 在控制台日志中检查时间线生成时间
  3. 验证崩溃日志中无崩溃记录
  4. 在旧设备上测试(不仅是最新iPhone)

Manual Testing Checklist

调试技巧

Basic Functionality:
  1. Add widget to Home Screen
  2. Verify it shows in widget gallery
  3. Check all supported sizes display correctly
  4. Confirm data matches app data
Data Updates:
  1. Change data in main app
  2. Observe widget updates (may take seconds)
  3. Force-quit app, verify widget still shows data
  4. Reboot device, verify widget persists
Edge Cases:
  1. Delete all app data, verify widget handles gracefully
  2. Disable network, verify widget works offline
  3. Enable Low Power Mode, verify widget respects limits
  4. Add multiple instances of same widget
Performance:
  1. Monitor memory usage in Xcode (Debug Navigator)
  2. Check timeline generation time in Console logs
  3. Verify no crashes in crash logs
  4. 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

症状:小组件未出现在小组件选择器中
诊断步骤
  1. 检查
    WidgetBundle
    是否包含你的小组件
  2. 验证
    supportedFamilies()
    已设置
  3. 检查扩展目标的"Skip Install"是否为NO
  4. 验证扩展的部署目标与应用匹配
解决方案
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:
  1. Check
    WidgetBundle
    includes your widget
  2. Verify
    supportedFamilies()
    is set
  3. Check extension target's "Skip Install" is NO
  4. Verify extension's deployment target matches app
Solution:
swift
@main
struct MyWidgetBundle: WidgetBundle {
    var body: some Widget {
        MyWidget()
        // Add your widget here if missing
    }
}
症状:小组件显示过期数据,不更新
诊断步骤
  1. 检查时间线策略(
    .atEnd
    vs
    .after()
    vs
    .never
  2. 验证未超过每日预算(40-70次重载)
  3. 检查
    getTimeline()
    是否被调用(添加日志)
  4. 确保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:
  1. Check timeline policy (
    .atEnd
    vs
    .after()
    vs
    .never
    )
  2. Verify you're not exceeding daily budget (40-70 reloads)
  3. Check if
    getTimeline()
    is being called (add logging)
  4. 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")
症状:小组件显示默认/空数据
诊断步骤
  1. 验证App Groups权限已在两个目标中配置
  2. 检查组标识符完全匹配
  3. 确保两个目标使用相同的suiteName
  4. 如果使用共享容器,检查文件路径
解决方案
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:
  1. Verify App Groups entitlement in BOTH targets
  2. Check group identifier matches exactly
  3. Ensure using same suiteName in both targets
  4. 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:
Activity.request()
throws error
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
}
症状:点击按钮无反应
诊断步骤
  1. 验证App Intent的
    perform()
    返回
    IntentResult
  2. 检查小组件目标中已导入intent
  3. 确保按钮使用
    intent:
    参数,而非
    action:
  4. 检查控制台中的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:
  1. Verify App Intent's
    perform()
    returns
    IntentResult
  2. Check intent is imported in widget target
  3. Ensure button uses
    intent:
    parameter, not
    action:
  4. 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")
}
症状:控件需要几秒响应,看起来冻结
原因
ControlValueProvider
或intent
perform()
中执行了同步操作
解决方案
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
ControlValueProvider
or intent
perform()
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
}
症状:小组件被裁剪或比例不正确
诊断步骤
  1. 检查视图代码中的
    entry.family
  2. 验证视图已适配类型尺寸
  3. 测试所有支持的类型
  4. 检查是否有硬编码的尺寸
解决方案
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:
  1. Check
    entry.family
    in view code
  2. Verify view adapts to family size
  3. Test all supported families
  4. 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上显示
诊断步骤
  1. 检查已设置
    .supplementalActivityFamilies([.small])
  2. 验证Apple Watch已配对且在附近
  3. 检查watchOS版本(11+)
  4. 确保蓝牙已启用
解决方案
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:
  1. Check
    .supplementalActivityFamilies([.small])
    is set
  2. Verify Apple Watch is paired and nearby
  3. Check watchOS version (11+)
  4. 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+