大多数浏览器和
Developer App 均支持流媒体播放。
-
为 Apple Watch 构建效率 App
您能在手腕上以前所未有的方式提高效率。了解如何利用 SwiftUI 和系统功能为 Apple Watch 构建一流的效率 App。我们将介绍如何为手表设计出色的工作体验,并探索如何进行文本输入、显示基本图表,以及与您的好友分享内容。
资源
相关视频
WWDC22
-
下载
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") } }
-
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) }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。