View in English

  • 打开菜单 关闭菜单
  • Apple Developer
搜索
关闭搜索
  • Apple Developer
  • 新闻
  • 探索
  • 设计
  • 开发
  • 分发
  • 支持
  • 账户
在“”范围内搜索。

快捷链接

5 快捷链接

视频

打开菜单 关闭菜单
  • 专题
  • 相关主题
  • 所有视频
  • 关于

更多视频

大多数浏览器和
Developer App 均支持流媒体播放。

  • 简介
  • 转写文稿
  • 代码
  • 为 Apple Watch 构建效率 App

    您能在手腕上以前所未有的方式提高效率。了解如何利用 SwiftUI 和系统功能为 Apple Watch 构建一流的效率 App。我们将介绍如何为手表设计出色的工作体验,并探索如何进行文本输入、显示基本图表,以及与您的好友分享内容。

    资源

    • Building a productivity app for Apple Watch
    • watchOS apps
      • 高清视频
      • 标清视频

    相关视频

    WWDC22

    • 认识 Swift Charts
    • Swift Charts:提高标准
    • SwiftUI 导航开发指南
    • SwiftUI 的新功能
    • Transferable 简介
  • 搜索此视频…

    Anne: 大家好 欢迎你们 我是 Anne Hitchcock 是 watchOS 软件工程师 今天 我想向您展示 如何在 watchOS 上创建 效率 App 自从 watchOS 6 中 引入了 SwiftUI 和独立的 Watch App 以来 您已经能够在 Watch App 中 做得比以往更多 每年 watchOS 上的 SwiftUI 都会有更多功能 同时 watchOS 也有了一些新功能 比如键盘 让您为 Watch 开发全新的 App 我想向您展示 如何将其中的一些功能组合起来 构建一个 App 来跟踪代办清单 我们将创建一个新的 Watch App 添加要显示的简单项目列表 让用户将项目添加至列表 然后进行编辑 当添加这些功能时 我们将讨论 Watch App 中 常见的导航策略 以及如何正确选择

    我们将与朋友分享条目 以分担负载

    然后会在我们的 App 中添加一个图表 以帮助我们发现效率趋势 并保持动力

    我们将使用数码表冠来制作 可滚动图表 以显示更大的数据范围

    我们从创建一个新的 App 开始

    在 Xcode 中创建一个新项目 在 watchOS 选项卡中 选择 App 并单击下一步

    选择 Product Name 后 您有几个选择 最重要的是 是创建 Watch 专用 App 还是带有配套 iOS App 的 Watch App 我们来谈谈一个优秀的 Watch App 的要素 以及何时需要配套的 iOS App

    出色的 Watch App 可实现快速交互 比如体能训练中的界面 让您快速开始最喜欢的体能训练 没人愿意定定站着 举起他们的手臂 然后才能点进去 尝试找到一些东西 出色的 Watch App 应该很容易 获得重要的信息 和功能

    出色的 Watch App 专注于其核心的目的 例如 天气 App 显示今天的天气预报 相关的当前情况 以及简单的 10 天天气预测

    专注于 App 中的关键内容 这样人们就可以轻松找到他们需要的 信息和行动

    一款优秀的 Watch App 会设计成可以独立于 配套的 iPhone 使用 例如 联系人 App 可以与手机同步 但不需要您的 iPhone 在附近 就可以访问您的 Apple Watch 上的 联系人信息

    您可能还想为您的 Watch App 安装配套的 iOS App 的原因有很多 包括提供 Apple Watch 捕获数据的历史记录 或者像健身 App 那样 提供详细的趋势分析

    由于我们的 App 具有集中的功能集 快速的交互和有限的数据 我们将创建一个 Watch 专用 App

    在这一点上 我想花几分钟 谈谈创建的目标

    如果您过去曾开发过 Watch App 那么您的 Watch 项目会有两个目标 WatchKit App 目标带有情节提要 材料 也许还有一些 与本地化相关的文件 以及包含所有 App 代码的 WatchKit 扩展目标 这些双重目标是 早期 watchOS 遗留下来的 并且确实没有充分的理由 再使用多个 Watch 目标了 从 Xcode 14 开始 新的 Watch App 有单一的 Watch App 目标 所有与 Watch App 相关的 代码 材料 本地化 Siri 意图以及 Widget 扩展 都包含在这个目标内

    好消息是 单一目标的 Watch App 可支持的最旧版本是 watchOS 7 您可以简化项目结构 并减少混乱和重复 同时也支持那些没有使用 最新的 watchOS 的用户

    如果您有现有的 App 带有 WatchKit 扩展目标 它将继续工作 您可以继续使用 Xcode 更新您的 App 并通过 App Store 发布您的 App

    如果您已经有一个 使用 SwiftUI lifecycle 的 Watch App 那么使用 Xcode 14 中的迁移工具 就可以很容易地过渡到单个目标 选择您的目标并从编辑器菜单中 选择 Validate Setting 如果您的部署目标 是 watchOS 7 或更高版本 将提供目标折叠选项

    如果您尚未实现飞跃 现在是时候开始 将您的 App 转换为使用 以享受单目标 Watch App 的简单性 以及 SwiftUI 的所有功能

    目标不是我们在 Xcode 14 中 进行的唯一简化功能 我们同时也让您能更轻松地 为 App 添加图标 现在 您仅需要一张 1024x1024 像素的图像就行了

    App 图标图像将按比例缩放 以便在所有 Watch 设备上显示

    请务必在设备的主屏幕 通知以及 iPhone 上 Watch App 的 设置中 使用您的 App 图标进行测试

    如有必要 您可以为 特定的较小尺寸添加自定义图像 例如 如果您的 App 图标在图像中 有一些细节在较小的尺寸中丢失了 您可以为这些尺寸 添加特定的图标图像 并删除图像细节 现在让我们通过添加任务项列表 来为 App 添加一些功能 我们将首先为任务列表 创建一个数据模型 ListItem 结构 将是可识别和可散列的 我们将给它一个要显示的描述

    然后创建一个简单的模型来存储数据 并发布列表项数组

    最后 添加模型作为环境对象 所以我们的视图 可以访问我们的模型

    现在使用数据模型 在 SwiftUI 中创建一个 List 由于目前还没有任务 当我们预览这个时 得到的是一个空列表

    我们得做点什么了 我们应该给人们一种将任务 添加到他们列表中的方法

    我们想添加一个按钮 用户可以点击该按钮 将新项目添加到列表中 文本字段链接是 watchOS 9 中的新功能 让您从按钮调用文本输入选项 并提供多种样式选择 使其在 App 中感觉自然且友好

    您可以用简单的字符串 创建一个基本的文本字段链接 或者使用 Label 来创建一个 更自定义的按钮

    使用视图修改器修改按钮外观 包括 foregroundColor foregroundStyle 和 buttonStyle

    我们将创建一个 AddItemLink 视图 以封装我们在 App 中使用的 文本字段链接的样式和行为

    我们将为按钮使用自定义标签 当有人输入文字时 我们会将新项目添加到列表中

    既然我们已经决定 使用文本字段链接 添加按钮来添加新的列表项 我们需要考虑 把文本字段链接放在哪里

    在 Watch App 中向列表添加操作时 有几个选择 对于短列表中的主要操作 在列表末尾使用按钮 导航链接或文本字段链接 将动作添加为列表末尾的项目 是短列表 如世界时钟中的 城市列表中的主要动作的不错选择 然而 如果您预计会有很长的项目清单 用户每次想要执行操作时 都必须继续滚动到列表的末尾 对于具有较长列表的常用操作 请使用工具栏项

    要添加工具栏项 请将工具栏修改器添加到列表中 并将操作视图用作内容 这将向列表中添加一个工具栏项 并自动放置工具栏项 虽然我想一直保持很短的 待办清单 但我很确定并不会 所以我要把文本字段链接 放在工具栏项中 使其易于访问

    让我们花点时间回顾一下 我们已经完成了什么 我们为列表项创建了一个模型 将其存储为环境对象 创建了一个显示项目的列表 并添加了一个文本字段链接 以添加新项目

    创建只有描述的项目 很简单 但不是很有用 我们需要将项目标记为完成 我们可能想要一种为任务 设置优先级的方法或添加 对工作量的估计 为此 我们将添加一个详细视图 这样做之前 我想回顾一下 Watch 上的 SwiftUI 中的 App 导航结构的选项 分层导航用于具有 列表-详细关系的视图 从 watchOS 9 开始 使用 SwiftUI NavigationStack 来创建使用这种类型的 导航结构的接口

    基于页面的导航用于 平面结构的视图 所有视图都是对等的

    基于页面的导航的一个很好的例子 是健身 App 的锻炼中视图 在锻炼过程中 人们可以轻松地在锻炼控制 指标 和回放控制之间滑动

    全屏 App 只有一个使用 整个显示的单一视图 这通常用于游戏等 App 和其他有一个主视图的 App

    对于全屏视图 请使用 ignoresSafeArea 修改器 将您的内容扩展到显示器的边缘 并且工具栏修改器的 可见性值为 hidden 以隐藏导航栏

    模态表是在当前视图上 滑动的全屏视图 它应作为 当前工作流的一部分完成的重要任务

    区分何时使用 分层流与何时使用模态图 非常重要

    邮件 App 使用分层样式显示消息列表 并显示每条消息或线程 作为详细视图 您可以从消息的详细信息中 执行一些操作 但在返回列表之前 不必做任何事

    如果您返回列表 然后点击“新信息” 邮件使用模态表显示 新信息视图 模态表是正确的选择 因为您需要填写新消息的详细信息 或在继续之前 选择取消

    要显示模态表 创建一个属性来控制工作表呈现状态 根据用户界面中的操作设置属性 并在表示状态属性为 true 时 使用工作表修改器 显示自定义模式工作表内容

    添加自定义工具栏项目到模态表 为您的项目添加一个工具栏 请注意 您的工具栏项目应使用 模态展示位置 像 confirmationAction cancellationAction 和 destructiveAction

    我们将使用模式表 作为详细信息视图 因为我们正在编辑一个项目 而且想专注于这个单一的任务 直到我们完成并点击完成

    要了解有关 SwiftUI 中导航的更多信息 包括关于 NavigationStack 的更多细节 和程序化导航 请观看“SwiftUI 导航开发指南”视频

    现在我们已经决定了 如何导航到详细视图 我们将更新列表项结构 我们有新的属性来存储估计工作 创建日期和完成日期

    让我们为用户提供一种 查看和编辑这些细节的方法

    我们将创建一个详细视图 其中包含用于编辑说明的文本字段 和一个用于将任务标记为 未完成或不完成的切换开关 但是我们应该怎么做估计的工作呢 我们知道值都将是数字 可以指定一个有效值的范围

    从 watchOS 9 开始 我们可以使用 Stepper 当您想要提供精细控制 来编辑顺序值时 Stepper 是个不错的选择

    您可以指定一个值的范围 并可选地提供步骤

    您还可以使用 Stepper 来编辑 逻辑顺序 但不一定是数值 例如 也许我们要注意 项目的估计压力水平

    我们可以创建一个表情符号数组 来表示压力水平 然后创建一个 Stepper 将值绑定到选定 在压力水平表情符号数组的索引中 并将范围设置为表情符号索引的范围 逐步执行这些值 会增加或减少 我们为项目估计的压力水平

    准备 WWDC 讲座很有趣 但与大家分享精彩的 Watch App 开发更是一个欢庆的派对 当我的清单上有压力项目时 或者只是我清单上的很多项目 我感到压力 我想与朋友分享我列表中的一个项目 来寻求帮助

    我们将把一个按钮 添加到我们的详细视图 允许用户使用共享表 来共享项目 我希望能够在我的详细视图中 点击一个按钮 来分享项目 从朋友列表中进行挑选 以寻求帮助 编辑我的消息 然后发送

    为此 我们将使用一个新工具 watchOS 9 上的 SwiftUI 可供我们使用 分享链接 我们可以通过创建包含项目 共享链接来共享列表项 我们可以有选择地 自定义带有主题和消息的 消息初始文本 当有人分享该项目时 提供预览显示在共享表 您可以使用 ShareLink 从 SwiftUI App 进行分享 在 iOS macOS 和 watchOS 中

    请务必查看“Transferable 简介”视频 了解更多 ShareLink 的详细信息 和选项 现在我可以跟踪完成的项目 并寻求帮助以完成工作 我还想添加图表 看看我的效率 我选择使用条形图 因为我只有一个数据序列 和不同的数据值 只要我限制一次显示的数据量 条形图将清楚地显示 Watch 显示屏上的这些数据 我们将首先添加 图表视图 到我们 App 的导航结构 我选择了基于页面的导航策略 因为在项目列表和图表之间 没有列表细节关系 有人可以在列表 和图表之间随时滑动

    为我们的列表和图表 添加基于页面的导航 让我们从创建一个 ItemList 结构开始 封装列表视图

    我把内容视图里的整个内容 移动到这个新项目列表 在这里封装项目列表 会让我们 在内容视图中拥有简单易读的 标签视图代码

    我们还需要为我们的图表视图 创建一个结构

    我会暂时放一个占位符 所以在我们构建图表之前 我们可以专注在导航结构上

    现在我们将用一个带有 两个标签的页面样式标签视图 来设置我们的内容视图 项目列表和图表

    既然我们已经设置了导航结构 让我们谈谈如何构建这个图表 我知道可以使用 SwiftUI Canvas 并绘制图表 但从 watchOS 9 开始 我们有一个更简单的答案 Swift Charts 也可以在 iOS 上使用 macOS 和 tvOS 因此您可以在任何使用 SwiftUI 的地方重复使用图表

    我们将汇总我们想要绘制图表的数据 然后让 Swift Charts 为我们显示

    对于图表 我们希望显示按日期完成的项目数 我们将创建一个结构 以存储图表的聚合数据

    然后我们会写一个小方法 来将列表项数据 聚合为图表数据元素

    通过指定要显示的数据 来显示一个简单的图表 并从数据中定义系列 我们使用日期作为 x 值 完成的项目数 作为 y 值

    在我的 Watch 显示屏上 实现我想要的样子 我正在使用图表的 chartXAxis 修改器 自定义 x 轴 我正在为轴值标签指定格式样式 我也不想要垂直网格线 所以我省略了一个 AxisGridLine 标记 我还使用chartYAxis 修改器 来自定义 y 轴 我指定了一个网格线样式看起来 很适合我在 Watch 上的图表 我正在格式化轴值标签 作为整数 并省略顶部标签 以防止它在图表的顶部被剪裁 想要了解更多 Swift Chart 可以实现的神奇功能 请观看“认识 Swift Charts” 和“Swift Charts:提高标准”视频

    我们的图表看起来不错 但我想在展示更多数据的同时 仍保持出色的观看体验 所以我要让它可滚动 为此 我们将使用 新的 digitalCrownRotation 修改器 它允许我们为数码表冠活动 设置回调 我们将实施 图表的自定义滚动行为

    让我们通过添加一些属性 来添加 digitalCrownRotation 修改器 以便当某人在图表上滚动时存储状态 highlightedDateIndex是当前滚动位置 数据点的日期索引

    我们将存储 crown offset 所以可以显示当前表冠位置 当这个人在滚动图表时 表冠随之移动 这是一个数据点上 或数据点之间的中间值

    要追踪是否有人正在进行滚动操作 我们将存储空闲状态 我们将使用这些信息 添加一点动画 随着表冠滚动停止和开始

    现在有了属性存储值 我们可以添加 数码表冠旋转修改器

    我们将把 detent 值 绑定到 highlightedDateIndex 属性

    在机械术语中 止动器是一种 保持某物在某个位置的系统 直到施加足够的力来移动它 例如 当我打开车门时 有一个停顿点 就是门将暂定的地方 我可以用力一点 把门打开到另一个停顿点 要关闭它 需要用力拉 克服阻力 把它拉离停顿点 否则 车门将弹回停顿点的位置 这就是止动器 车门的停顿点 帮助我们了解这个 API 中的 detent Detent 是您的视图中 表冠的静止槽口位置

    在 onChange 回调的处理程序中 我们将 isCrownIdle 的值 设置为错误 因为我们知道表冠在滚动 我们将表冠偏移值设置为当前值 让我们在滚动期间 显示图表上的当前位置

    在 onIdle 回调的处理程序中 我们将 isCrownIdle 的值设置为真

    现在我们可以在图表滚动时 显示表冠的位置 为此 我们可以使用 Swift Charts 中的 RuleMark 类型 RuleMark 是图表上的一条直线 您可以用它来显示水平线或垂直线 以显示阈值 例如 显示斜线

    我们将创建一个 RuleMark 具有表冠偏移日期值 以显示表冠滚动的当前位置

    为了让它看起来更美观 我想让表冠位置线褪色 当表冠停止移动时 对此进行动画处理非常简单 使用我们添加的 isCrownIdle 属性

    我们将添加一个属性来存储 我们在 foregroundStyle 中 为 RuleMark 使用颜色的不透明度

    在图表中添加一个 onChange 修改器 当 isCrownIdle 值发生变化时 可以使 crownPositionOpacity 值发生变化

    然后更新 RuleMark 的前景样式 以使用不透明度

    要在滚动时在图表条形图旁边显示值 我们可以向 BarMark 添加注释 我们将注释放置在栏的顶部前导侧 当它是最后一栏时 否则 我们将它定位在 顶部尾随侧

    让我们一起来看看 我们的成果 只需使用 digitalCrownRotation 修改器 Swift Charts 中的 RuleMark 和一个简单的 SwiftUI 动画

    创建自定义可滚动图表的最后一步 在有人进行滚动操作时 调整图表的数据范围 创建一个属性来存储可见范围

    创建 chartData 变量 以向图表提供区域内的数据 当 highlightedDateIndex 更改时 调用一个方法来检查图表数据范围 并在必要时进行更新

    当有人使用数码表冠 滚动图表时 图表将滚动以显示可用数据 现在 我们已经完成了 我们计划的所有功能的实现

    想了解有关在 watchOS 9 中可用的 新的 SwiftUI 功能的更多信息 请查看“SwiftUI 的新功能” 在规划 Watch App 或您的新 Watch App 功能时 请想想什么能帮助打造 出色的 Watch App 体验 在设计您的 App 时 考虑一下您的 App 导航策略 以确保您的 App 简单 直观 使用 SwiftUI 获得更简单 更丰富的开发选项 继续构建出色的 Watch App 请记住 因为有您 我们确定世上有 App 可以做到这一点

    • 6:12 - Initial ListItem struct

      struct ListItem: Identifiable, Hashable {
          
          let id = UUID()
          var description: String
          
          init(_ description: String) {
              self.description = description
          }
      }
    • 6:24 - ItemListModel

      class ItemListModel: NSObject, ObservableObject {
          @Published var items = [ListItem]()
      }
    • 6:30 - Add the ItemListModel as an EnvironmentObject

      @main
      struct WatchTaskListSampleApp: App {
          
          @StateObject var itemListModel = ItemListModel()
          
          @SceneBuilder var body: some Scene {
              WindowGroup {
                  ContentView()
                      .environmentObject(itemListModel)
              }
          }
      }
    • 6:37 - Create a simple SwiftUI List

      struct ContentView: View {
          @EnvironmentObject private var model: ItemListModel
          
          var body: some View {
              List {
                  ForEach($model.items) { $item in
                      ItemRow(item: $item)
                  }
                  
                  if model.items.isEmpty {
                      Text("No items to do!")
                          .foregroundStyle(.gray)
                  }
              }
              .navigationTitle("Tasks")
          }
      }
    • 7:11 - TextFieldLink with a simple String

      struct ContentView: View {
          @EnvironmentObject private var model: ItemListModel
          
          var body: some View {
              VStack {
                  TextFieldLink("Add") {
                      model.items.append(ListItem($0))
                  }
              }
              .navigationTitle("Tasks")
          }
      }
    • 7:16 - TextFieldLink with a Label

      struct ContentView: View {
          @EnvironmentObject private var model: ItemListModel
          
          var body: some View {
              VStack {
                  TextFieldLink {
                      Label(
                          "Add", 
                          systemImage: "plus.circle.fill")
                  } onSubmit: {
                      model.items.append(ListItem($0))
                  }
              }
              .navigationTitle("Tasks")
          }
      }
    • 7:20 - TextFieldLink with foregroundStyle modifier

      struct ContentView: View {
          @EnvironmentObject private var model: ItemListModel
          
          var body: some View {
              VStack {
                  TextFieldLink {
                      Label(
                          "Add", 
                          systemImage: "plus.circle.fill")
                  } onSubmit: {
                      model.items.append(ListItem($0))
                  }
                  .foregroundStyle(.tint)
              }
              .navigationTitle("Tasks")
          }
      }
    • 7:27 - TextFieldLink with buttonStyle modifier

      struct ContentView: View {
          @EnvironmentObject private var model: ItemListModel
          
          var body: some View {
              VStack {
                  TextFieldLink {
                      Label(
                          "Add", 
                          systemImage: "plus.circle.fill")
                  } onSubmit: {
                      model.items.append(ListItem($0))
                  }
                  .buttonStyle(.borderedProminent)
              }
              .navigationTitle("Tasks")
          }
      }
    • 7:30 - Create the AddItemLink View to encapsulate the style and behavior of the TextFieldLink to add list items

      struct AddItemLink: View {
          @EnvironmentObject private var model: ItemListModel
          
          var body: some View {
              TextFieldLink(prompt: Text("New Item")) {
                  Label("Add",
                        systemImage: "plus.circle.fill")
              } onSubmit: {
                  model.items.append(ListItem($0))
              } 
          }
      }
    • 8:38 - Add a toolbar item to allow people to add new list items

      struct ContentView: View {
          @EnvironmentObject private var model: ItemListModel
          
          var body: some View {
              List {
                  ForEach($model.items) { $item in
                      ItemRow(item: $item)
                  }
                  
                  if model.items.isEmpty {
                      Text("No items to do!")
                          .foregroundStyle(.gray)
                  }
              }
              .toolbar {
                  AddItemLink()
              }
              .navigationTitle("Tasks")
          }
      }
    • 11:40 - Display a modal sheet

      struct ItemRow: View {
          @EnvironmentObject private var model: ItemListModel
          
          @Binding var item: ListItem
          @State private var showDetail = false
          
          var body: some View {
              Button {
                  showDetail = true
              } label: {
                  HStack {
                      Text(item.description)
                          .strikethrough(item.isComplete)
                      Spacer()
                      Image(systemName: "checkmark").opacity(item.isComplete ? 100 : 0)
                  }
              }
              .sheet(isPresented: $showDetail) {
                  ItemDetail(item: $item)
              }
          }
      }
    • 11:58 - Display a modal sheet with custom toolbar items

      struct ItemRow: View {
          @EnvironmentObject private var model: ItemListModel
          
          @Binding var item: ListItem
          @State private var showDetail = false
          
          var body: some View {
              Button {
                  showDetail = true
              } label: {
                  HStack {
                      Text(item.description)
                          .strikethrough(item.isComplete)
                      Spacer()
                      Image(systemName: "checkmark").opacity(item.isComplete ? 100 : 0)
                  }
              }
              .sheet(isPresented: $showDetail) {
                  ItemDetail(item: $item)
                      .toolbar {
                          ToolbarItem(placement: .confirmationAction) {
                              Button("Done") {
                                  showDetail = false
                              }
                          }
                      }
              }
          }
      }
    • 12:36 - Add more properties to the ListItem

      struct ListItem: Identifiable, Hashable {
          
          let id = UUID()
          var description: String
          var estimatedWork: Double = 1.0
          var creationDate = Date()
          var completionDate: Date?
          
          init(_ description: String) {
              self.description = description
          }
      
          var isComplete: Bool {
              get {
                  completionDate != nil
              }
              set {
                  if newValue {
                      guard completionDate == nil else { return }
                      completionDate = Date()
                  } else {
                      completionDate = nil
                  }
              }
          }
      }
    • 12:48 - Create the ItemDetail View with the Stepper

      struct ItemDetail: View {
          @Binding var item: ListItem
          
          var body: some View {
              Form {
                  Section("List Item") {
                      TextField("Item", text: $item.description, prompt: Text("List Item"))
                  }
                  Section("Estimated Work") {
                      Stepper(value: $item.estimatedWork,
                              in: (0.0...14.0),
                              step: 0.5,
                              format: .number) {
                          Text("\(item.estimatedWork, specifier: "%.1f") days")
                      }
                  }
                  
                  Toggle(isOn: $item.isComplete) {
                      Text("Completed")
                  }
              }
          }
      }
    • 13:29 - A Stepper with Emoji

      // Use a Stepper to edit the stress level of an item
      struct StressStepper: View {
          private let stressLevels = [
              "😱", "😡", "😳", "🙁", "🫤", "🙂", "🥳"
          ]
          @State private var stressLevelIndex = 5
          
          var body: some View {
              VStack {
                  Text("Stress Level")
                      .font(.system(.footnote, weight: .bold))
                      .foregroundStyle(.tint)
                  
                  Stepper(value: $stressLevelIndex,
                          in: (0...stressLevels.count-1)) {
                      Text(stressLevels[stressLevelIndex])
                  }
              }
          }
      }
    • 14:43 - Add a ShareLink to the ItemDetail View

      struct ItemDetail: View {
          @Binding var item: ListItem
          
          var body: some View {
              Form {
                  Section("List Item") {
                      TextField("Item", text: $item.description, prompt: Text("List Item"))
                  }
                  Section("Estimated Work") {
                      Stepper(value: $item.estimatedWork,
                              in: (0.0...14.0),
                              step: 0.5,
                              format: .number) {
                          Text("\(item.estimatedWork, specifier: "%.1f") days")
                      }
                  }
                  
                  Toggle(isOn: $item.isComplete) {
                      Text("Completed")
                  }
                  
                  ShareLink(item: item.description,
                            subject: Text("Please help!"),
                            message: Text("(I need some help finishing this.)"),
                            preview: SharePreview("\(item.description)"))
                  .buttonStyle(.borderedProminent)
                  .buttonBorderShape(.roundedRectangle)
                  .listRowInsets(
                      EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
                  )
              }
          }
      }
    • 16:39 - Page-style TabView with navigation titles for each page

      struct ContentView: View {
          var body: some View {
              TabView {
                  NavigationStack {
                      ItemList()
                  }
                  NavigationStack {
                      ProductivityChart()
                  }
              }.tabViewStyle(.page)
          }
      }
    • 17:20 - ChartData struct for aggregate data

      /// Aggregate data for charting productivity.
      struct ChartData {
          struct DataElement: Identifiable {
              var id: Date { return date }
              let date: Date
              let itemsComplete: Double
          }
          
          /// Create aggregate chart data from list items.
          /// - Parameter items: An array of list items to aggregate for charting.
          /// - Returns: The chart data source.
          static func createData(_ items: [ListItem]) -> [DataElement] {
              return Dictionary(grouping: items, by: \.completionDate)
                  .compactMap {
                      guard let date = $0 else { return nil }
                      return DataElement(date: date, itemsComplete: Double($1.count))
                  }
                  .sorted {
                      $0.date < $1.date
                  }
          }
      }
    • 17:36 - Static sample data for chart and basic bar chart

      extension ChartData {
          
          /// Some static sample data for displaying a `Chart`.
          static var chartSampleData: [DataElement] {
              let calendar = Calendar.autoupdatingCurrent
              var startDateComponents = calendar.dateComponents(
                  [.year, .month, .day], from: Date())
              startDateComponents.setValue(22, for: .day)
              startDateComponents.setValue(5, for: .month)
              startDateComponents.setValue(2022, for: .year)
              startDateComponents.setValue(0, for: .hour)
              startDateComponents.setValue(0, for: .minute)
              startDateComponents.setValue(0, for: .second)
              let startDate = calendar.date(from: startDateComponents)!
              
              let itemsToAdd = [
                  6, 3, 1, 4, 1, 2, 7,
                  5, 2, 0, 5, 2, 3, 9
              ]
              var items = [DataElement]()
              for dayOffset in (0..<itemsToAdd.count) {
                  items.append(DataElement(
                      date: calendar.date(byAdding: .day, value: dayOffset, to: startDate)!,
                      itemsComplete: Double(itemsToAdd[dayOffset])))
              }
              
              return items
          }
      }
      
      struct ProductivityChart: View {
             
          let data = ChartData.createData(
              ListItem.chartSampleData)
                   
          var body: some View {
              Chart(data) { dataPoint in
                  BarMark(
                      x: .value("Date", dataPoint.date),
                      y: .value(
                          “Completed", 
                          dataPoint.itemsComplete)
                  )
                  .foregroundStyle(Color.accentColor)
              }
              .navigationTitle("Productivity")
              .navigationBarTitleDisplayMode(.inline)
          }
      }
    • 17:50 - Chart with chartXAxis modifier

      struct ProductivityChart: View {
             
          let data = ChartData.createData(
              ListItem.chartSampleData)
        
          private var shortDateFormatStyle = DateFormatStyle(dateFormatTemplate: "Md")
                   
          var body: some View {
              Chart(data) { dataPoint in
                  BarMark(
                      x: .value("Date", dataPoint.date),
                      y: .value(
                          “Completed", 
                          dataPoint.itemsComplete)
                  )
                  .foregroundStyle(Color.accentColor)
              }
            	.chartXAxis {
                  AxisMarks(format: shortDateFormatStyle)
              }
              .navigationTitle("Productivity")
              .navigationBarTitleDisplayMode(.inline)
          }
      }
      
      /// `ProductivityChart` uses this type to format the dates on the x-axis.
      struct DateFormatStyle: FormatStyle {
          enum CodingKeys: CodingKey {
              case dateFormatTemplate
          }
          
          private var dateFormatTemplate: String
          private var formatter: DateFormatter
          
          init(dateFormatTemplate: String) {
              self.dateFormatTemplate = dateFormatTemplate
              formatter = DateFormatter()
              formatter.locale = Locale.autoupdatingCurrent
              formatter.setLocalizedDateFormatFromTemplate(dateFormatTemplate)
          }
          
          init(from decoder: Decoder) throws {
              let container = try decoder.container(keyedBy: CodingKeys.self)
              dateFormatTemplate = try container.decode(String.self, forKey: .dateFormatTemplate)
              formatter = DateFormatter()
              formatter.setLocalizedDateFormatFromTemplate(dateFormatTemplate)
          }
          
          func encode(to encoder: Encoder) throws {
              var container = encoder.container(keyedBy: CodingKeys.self)
              try container.encode(dateFormatTemplate, forKey: .dateFormatTemplate)
          }
          
          func format(_ value: Date) -> String {
              formatter.string(from: value)
          }
      }
    • 19:05 - Add the digitalCrownRotation modifier

      struct ProductivityChart: View {
             
          let data = ChartData.createData(
              ListItem.chartSampleData)
      
          /// The index of the highlighted chart value. This is for crown scrolling.
          @State private var highlightedDateIndex: Int = 0
      
          /// The current offset of the crown while it's rotating. This sample sets the offset with
          /// the value in the DigitalCrownEvent and uses it to show an intermediate
          /// (between detents) chart value in the view.
          @State private var crownOffset: Double = 0.0
      
          @State private var isCrownIdle = true
        
          private var chart: some View {
              Chart(data) { dataPoint in
                  BarMark(
                      x: .value("Date", dataPoint.date),
                      y: .value(
                          “Completed", 
                          dataPoint.itemsComplete)
                  )
                  .foregroundStyle(Color.accentColor)
              }
            	.chartXAxis {
                  AxisMarks(format: shortDateFormatStyle)
              }
          }
              
          var body: some View {
              chart
                  .focusable()
                  .digitalCrownRotation(
                      detent: $highlightedDateIndex,
                      from: 0,
                      through: data.count - 1,
                      by: 1,
                      sensitivity: .medium
                  ) { crownEvent in
                      isCrownIdle = false
                      crownOffset = crownEvent.offset
                  } onIdle: {
                      isCrownIdle = true
                  }
                  .navigationTitle("Productivity")
                  .navigationBarTitleDisplayMode(.inline)
          }
      }
    • 21:07 - Add a RuleMark to the Chart to show the current Digital Crown position

      /// The date value that corresponds to the crown offset.
      private var crownOffsetDate: Date {
          let dateDistance = data[0].date.distance(
              to: data[data.count - 1].date) * (crownOffset / Double(data.count - 1))
          return data[0].date.addingTimeInterval(dateDistance)
      }
      
      private var chart: some View {
          Chart(data) { dataPoint in
              BarMark(
                  x: .value("Date", dataPoint.date),
                  y: .value(
                      "Completed", 
                      dataPoint.itemsComplete)
              )
              .foregroundStyle(Color.accentColor)
                   
              RuleMark(x: .value("Date", crownOffsetDate))
                  .foregroundStyle(Color.appYellow)
          }
          .chartXAxis {
              AxisMarks(format: shortDateFormatStyle)
          }
      }
    • 21:37 - Add animation to dim the crown position line when the scrolling idle state changes

      struct ProductivityChart: View {
             
          let data = ChartData.createData(
              ListItem.chartSampleData)
      
          /// The index of the highlighted chart value. This is for crown scrolling.
          @State private var highlightedDateIndex: Int = 0
      
          /// The current offset of the crown while it's rotating. This sample sets the offset with
          /// the value in the DigitalCrownEvent and uses it to show an intermediate
          /// (between detents) chart value in the view.
          @State private var crownOffset: Double = 0.0
      
          @State private var isCrownIdle = true
      
          @State var crownPositionOpacity: CGFloat = 0.2
        
          private var chart: some View {
              Chart(data) { dataPoint in
                  BarMark(
                      x: .value("Date", dataPoint.date),
                      y: .value(
                          “Completed", 
                          dataPoint.itemsComplete)
                  )
                  .foregroundStyle(Color.accentColor)
                           
                  RuleMark(x: .value("Date", crownOffsetDate))
                      .foregroundStyle(Color.appYellow.opacity(crownPositionOpacity))
              }
            	.chartXAxis {
                  AxisMarks(format: shortDateFormatStyle)
              }
          }
                   
          var body: some View {
              chart
                  .focusable()
                  .digitalCrownRotation(
                      detent: $highlightedDateIndex,
                      from: 0,
                      through: data.count - 1,
                      by: 1,
                      sensitivity: .medium
                  ) { crownEvent in
                      isCrownIdle = false
                      crownOffset = crownEvent.offset
                  } onIdle: {
                      isCrownIdle = true
                  }
                  .onChange(of: isCrownIdle) { newValue in
                      withAnimation(newValue ? .easeOut : .easeIn) {
                          crownPositionOpacity = newValue ? 0.2 : 1.0
                      }
                  }
                  .navigationTitle("Productivity")
                  .navigationBarTitleDisplayMode(.inline)
          }
      }
    • 22:14 - Add an annotation to the bar chart to display the current value

      private func isLastDataPoint(_ dataPoint: ChartData.DataElement) -> Bool {
          data[chartDataRange.upperBound].id == dataPoint.id
      }
      
      private var chart: some View {
          Chart(chartData) { dataPoint in
              BarMark(x: .value("Date", dataPoint.date, unit: .day),
              y: .value("Completed", dataPoint.itemsComplete))
              .foregroundStyle(Color.accentColor)
              .annotation(
                  position: isLastDataPoint(dataPoint) ? .topLeading : .topTrailing,
                  spacing: 0
              ) {
                  Text("\(dataPoint.itemsComplete, format: .number)")
                      .foregroundStyle(dataPoint.date == crownOffsetDate ? Color.appYellow : Color.clear)
              }
      
              RuleMark(x: .value("Date", crownOffsetDate, unit: .day))
                  .foregroundStyle(Color.appYellow.opacity(crownPositionOpacity))
          }
          .chartXAxis {
              AxisMarks(format: shortDateFormatStyle)
          }
      }
    • 22:44 - Make the chart data range scrollable

      @State var chartDataRange = (0...6)
      
      private func updateChartDataRange() {
          if (highlightedDateIndex - chartDataRange.lowerBound) < 2, chartDataRange.lowerBound > 0 {
              let newLowerBound = max(0, chartDataRange.lowerBound - 1)
              let newUpperBound = min(newLowerBound + 6, data.count - 1)
              chartDataRange = (newLowerBound...newUpperBound)
              return
          }
          if (chartDataRange.upperBound - highlightedDateIndex) < 2, chartDataRange.upperBound < data.count - 1 {
              let newUpperBound = min(chartDataRange.upperBound + 1, data.count - 1)
              let newLowerBound = max(0, newUpperBound - 6)
              chartDataRange = (newLowerBound...newUpperBound)
              return
          }
      }
      
      private var chartData: [ChartData.DataElement] {
          Array(data[chartDataRange.clamped(to: (0...data.count - 1))])
      }
      
      private var chart: some View {
          Chart(chartData) { dataPoint in
              BarMark(x: .value("Date", dataPoint.date, unit: .day),
                      y: .value("Completed", dataPoint.itemsComplete)
              )
              .foregroundStyle(Color.accentColor)
              .annotation(
                  position: isLastDataPoint(dataPoint) ? .topLeading : .topTrailing,
                  spacing: 0
              ) {
                  Text("\(dataPoint.itemsComplete, format: .number)")
                      .foregroundStyle(dataPoint.date == crownOffsetDate ? Color.appYellow : Color.clear)
              }
      
              RuleMark(x: .value("Date", crownOffsetDate, unit: .day))
                  .foregroundStyle(Color.appYellow.opacity(crownPositionOpacity))
          }
          .chartXAxis {
              AxisMarks(format: shortDateFormatStyle)
          }
      }
      
      var body: some View {
          chart
              .focusable()
              .digitalCrownRotation(
                  detent: $highlightedDateIndex,
                  from: 0,
                  through: data.count - 1,
                  by: 1,
                  sensitivity: .medium
              ) { crownEvent in
                  isCrownIdle = false
                  crownOffset = crownEvent.offset
              } onIdle: {
                  isCrownIdle = true
              }
              .onChange(of: isCrownIdle) { newValue in
                  withAnimation(newValue ? .easeOut : .easeIn) {
                      crownPositionOpacity = newValue ? 0.2 : 1.0
                  }
              }
              .onChange(of: highlightedDateIndex) { newValue in
                  withAnimation {
                      updateChartDataRange()
                  }
              }
              .navigationTitle("Productivity")
              .navigationBarTitleDisplayMode(.inline)
      }

Developer Footer

  • 视频
  • WWDC22
  • 为 Apple Watch 构建效率 App
  • 打开菜单 关闭菜单
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    打开菜单 关闭菜单
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    打开菜单 关闭菜单
    • 辅助功能
    • 配件
    • App 扩展
    • App Store
    • 音频与视频 (英文)
    • 增强现实
    • 设计
    • 分发
    • 教育
    • 字体 (英文)
    • 游戏
    • 健康与健身
    • App 内购买项目
    • 本地化
    • 地图与位置
    • 机器学习
    • 开源资源 (英文)
    • 安全性
    • Safari 浏览器与网页 (英文)
    打开菜单 关闭菜单
    • 完整文档 (英文)
    • 部分主题文档 (简体中文)
    • 教程
    • 下载 (英文)
    • 论坛 (英文)
    • 视频
    打开菜单 关闭菜单
    • 支持文档
    • 联系我们
    • 错误报告
    • 系统状态 (英文)
    打开菜单 关闭菜单
    • Apple 开发者
    • App Store Connect
    • 证书、标识符和描述文件 (英文)
    • 反馈助理
    打开菜单 关闭菜单
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program (英文)
    • News Partner Program (英文)
    • Video Partner Program (英文)
    • 安全赏金计划 (英文)
    • Security Research Device Program (英文)
    打开菜单 关闭菜单
    • 与 Apple 会面交流
    • Apple Developer Center
    • App Store 大奖 (英文)
    • Apple 设计大奖
    • Apple Developer Academies (英文)
    • WWDC
    获取 Apple Developer App。
    版权所有 © 2025 Apple Inc. 保留所有权利。
    使用条款 隐私政策 协议和准则