大多数浏览器和
Developer App 均支持流媒体播放。
-
SwiftUI 新功能
SwiftUI 可以帮助你为 iPhone、iPad、Mac、Apple Watch 和 Apple TV 构建更精美强大的 app。进一步了解更多有关 SwiftUI 的最新进展,包括界面改进(如轮廓,网格和工具栏)。利用 SwiftUI 对 Apple 框架的增强支持来启用诸如“通过 Apple 登录”等功能。了解新的视觉效果以及新的控件和样式;以及新的 app 和场景 API 如何让你得以完全在 SwiftUI 中创建 app、自定义复杂性和所有新的小组件。 为了充分利用本节内容,你首先需要熟悉 SwiftUI。请观看“SwiftUI 介绍”获取入门知识。
资源
相关视频
WWDC20
-
下载
(你好 WWDC 2020) 你好 欢迎参加 WWDC 2020 (SwiftUI 新功能) 我是 Matt Ricketson 就职于 SwiftUI 团队 (SwiftUI 工程师 Matt Ricketson SwiftUI 工程师 Taylor Kelly) 待会我的同事 Taylor 会来介绍 去年 我们介绍了 SwiftUI 这是在 Apple 的全体平台中 打造卓越用户界面的强大新方法 在 SwiftUI 的第二次重大发布中 向你展示这些创新 让我们兴奋不已
你很快就会发现 今年的新功能不胜枚举 一次讲座远远无法面面俱到 不过我们会尽量多介绍 我们也会让你了解其他讲座 你就能进一步了解 首先 我们要介绍新 app 和小组件 API
我们还将讨论显示列表与集合方面的改进
我们会介绍适用于工具栏和控件的 新款多平台 API 还会展示各种打造风格化 app 的 新视觉效果
最后会探讨新方法 让你的 SwiftUI app 融入系统其他部分 不过咱们先说说 app 和小组件吧 这是首次 仅用 SwiftUI 就能做出整个 app 而不用将 SwiftUI 代码嵌入 UIKit AppKit 或 WatchKit app 了 咱们来看一看 你在这看到的是一个完整的 SwiftUI app 一个简单的“你好 世界!” 的例子 (你好 世界!) 没错 那是个百分百正常运行的 app 你可以构建并运行这段代码
其实 它如此简洁 你能把整个 app 装进 区区 140 字符的代码中 不过别被这个骗了 SwiftUI 为声明式 app 将大量智能、自动、可定制的行为 装进了一个简单灵活的 API 我写了一个 app 为了追踪 我在读书会中正在阅读的书籍 在底部 我写了一个定制视图 代表 app 的主用户界面 在顶部 我将该视图作为 app 的主窗口内容 这里首先要注意的是这两个声明有多相似 我们将 SwiftUI 的新 app API 设计为 遵循你在视图代码中已经习惯的 声明式的状态驱动模式
在这两种情况中 你都是在定义一个 遵循协议的结构体 可以使用属性声明数据依赖关系 且该数据是在主体属性中使用的 对 app 和视图而言 此数据定义了 它们的声明性用户界面内容
不过你可能注意到了一个关键区别 就是 app 的主体属性的返回类型 一个 app 的主体会返回一个场景 这是 SwiftUI 的一个新概念 代表可以由平台 独立显示的 app 的用户界面 我们准备了一个更深入的全面介绍 关于场景是什么 以及它们如何与 app 和视图相关联 (SwiftUI 中的 app 要点) 现在 我只想关注我们在这个 名为 WindowGroup 的应用中 使用的场景 因为 WindowGroup 是一个很有力的例子 证明了 SwiftUI 中的场景如何能 创造性地提供智能 多平台的功能
在我们的 iOS app 中 WindowGroup 正在为之 创建和管理单个的全屏窗口 不过同样的代码也能在 watchOS 上运行 也是管理单个的全屏窗口 当然 我们的 watchOS app 看起来 和我们的 iOS app 不一样 但两个平台上的核心 app 结构是一样的 这就能让它们共享同一个 app 声明 其实我的 app 在 tvOS 和 iPad 上也能用 鉴于 iPadOS 支持多窗口 app 那我们就能免费获得额外功能
例如能够运行并排显示的 多个副本
此功能拓展到了 macOS 它也支持多窗口 我能使用标准 command-N 快捷键 创建新窗口 然后将它们聚集到有选项卡的单个窗口中
SwiftUI 甚至会自动往我的主菜单中 添加一个新窗口菜单指令
这些能实现 全靠这个简单的 app 声明 它使用新的 WindowGroup API 来定义我的界面
SwiftUI 也支持其他类型的场景 它们可以像视图一样组合在一起 以构建更复杂的 app
比如 macOS 中可用的新设置场景 能向你的 Mac app 添加一个偏好窗口 设置场景会在 app 菜单中 自动设置标准偏好指令 还能对窗口进行正确的风格处理
SwiftUI 的场景 API 也支持 基于文档的 app 比如这个我做出来画矢量图的 app
今年新增了 DocumentGroup 场景类型 它会自动管理开启、编辑 和保存基于文档的场景 iOS iPadOS 和 macOS 平台都支持此功能
在 iOS 和 iPadOS 中 DocumentGroup 会自动呈现出一个文档浏览器 如何没提供其他主界面的话 在 Mac 中 DocumentGroup 会为每个新文档打开不同的窗口 还会自动为主菜单添加指令 用于普通的文档操作
说到菜单指令 SwiftUI 也能使用新的指令修改器 让你添加额外指令
例如 我在这添加了一个自定义形状菜单 用来向画布添加新形状 macOS 会自动将自定义菜单 添加到主菜单的正确分区 还会显示快捷键 这些是我们使用新的 keyboardShortcut 视图修改器指定的
指令 API 的功能远远不止 我们此处所展示的 例如它还能基于用户关注点来完成指令 使用起来很有趣 你能查看参考说明文档来进一步了解
关于 app 和场景还有很多可以介绍的 我们准备了另外几场讲座 帮你深入了解这些新的 API “SwiftUI 中的 app 要点” 讲解了视图、场景和 app 如何更深度地协同运作 “SwiftUI 中基于文档的 app” 深入介绍如何在你的 app 中 打开和管理文档
为了帮助你构建这些新 app 我们还通过添加 为 SwiftUI app 打造的新款多平台模板 更新了 Xcode 中的“新项目”体验
这些新模板针对多平台代码进行了优化 能自动为分享代码 以及平台专用的组件和资产来设置分组
我们在扩展的另一部分项目体验 是如何配置你的 app 的启动页面
今年新增了启动页面 info.plist 密钥
你可以声明标准启动页面组件的各种组合 例如默认图像、背景颜色 和空白顶部和底部栏 就像我在这配置的
你可能已经为启动页面 使用了 Storyboard 它的效果依然出色 没有理由换掉 但对于不使用 Storyboard 的 新 SwiftUI 项目而言 启动页面配置就是个简单的备选 现在来介绍小组件 这是 iOS iPadOS 和 macOS 中令人激动的新功能
小组件是专门用 SwiftUI 打造的
构造小组件就与 app 和视图一样 是使用一个遵循新小组件协议的 自定义结构来操作 你可以制作很多不同类型的小组件 比如这个 它会定期推荐新专辑给我听
小组件也能用其他种类的数据来配置 例如 Siri intents
说到构建小组件 能介绍的就太多了 我们有若干讲座帮你开始了解 如需来进一步了解 我想推荐你观看 “为小组件构建 SwiftUI 视图” (小组件边看边写:1-3 部)
你现在终于能使用 SwiftUI 为 Apple Watch 构建自定义复杂功能了 你可以构建全彩色复杂功能 比如我制作的这份每周咖啡图表 还能定制它在有色表盘上的外观 比如我喜欢用的这种清爽蓝色调
如需进一步了解 请查看 “在 SwiftUI 中构建复杂功能” 如果你是构建复杂功能的新手 那我推荐你先观看 “在 Apple Watch 中创建复杂功能” 我们接下来讲讲对显示列表与集合的改进
列表是许多 app 的关键组件 通常代表用户与之交互的首要界面
在这个版本中 列表会获得一些卓越的新功能 我对大纲的新支持尤为感到兴奋 常规列表支持动态的 以数据驱动的内容的简洁声明
通过为其初始化程序提供子键路径 列表现在可以构建内容的递归大纲 默认情况下 这将在 macOS iOS 和 iPadOS 上 采用预期的系统标准样式显示 我们希望这种易于使用的大纲能有助于 减少对专注于内容的 app 中的 干扰性推送和弹出导航模式的需求 除了列表和大纲 用其他类型的可滚动布局 如网格 来显示内容集合也很常见
今年 SwiftUI 增加了 对延迟加载网格布局的支持 可以结合滚动视图 来创建平滑滚动的内容网格
网格是一种强大的布局 支持各种不同的配置 比如调整列的数量以适应可用空间 就像我们在这里看到的横屏和竖屏一样
或者强制设置固定数量的列 让每个列都有各自的尺寸调整参数 例如这个 在每个方向上都固定四列 当然 SwiftUI 也支持横向滚动网格 我们还展示了现有垂直和水平叠放布局的 延迟加载版本 这些版本非常适合构建自定义可滚动布局 比如这个不对称的图库 咱们来仔细看看
我们在这使用一个包含所有图库内容的 延迟垂直叠放
我们还为开关语句 使用了新的视图构建器支持 就能在叠放的不同图像布局间轻松切换 例如在顶部显示的单个大图
不对称的一组三个图像
以及较短几行的较小图像 再加上一个延迟加载的垂直滚动叠放 它们构成了一个无缝的图库 列表和集合都是 SwiftUI 的强大功能 我们这场讲座只是简单介绍了它的功能 如需进一步了解 就真的应该看看 我们关于叠放、网格和大纲的讲座 现在交给 Taylor 来介绍工具栏和控件 谢谢 Matt 使用 SwiftUI 凭借新的 DocumentGroup 和集合视图 让我们的 app 模型变得栩栩如生 这真是太酷了 现在来看看 SwiftUI 支持的强大工具栏 和定制控件的新方法吧 我们全平台的工具栏和 app 都有一些出色的更新 从 macOS Big Sur 的漂亮新外观 到更新后的 iPad 系统体验 再到 watchOS 的基本操作 今年 SwiftUI 有了一个新的 API 可以使用新的工具栏修改器 来构造所有这些更新
工具栏条目由你在整个 SwiftUI 中 使用的视图组成 在本例中是一个按钮
默认情况下 它们会被放置在惯用的位置 但可以通过使用工具栏项明确定制 在本例中 基本操作 就是 watchOS 上的默认摆放 不过也有其他摆放方式 例如 确认和取消模态操作 这些都是语义摆放的例子 你向 SwiftUI 描述 这些工具栏条目的角色时 SwiftUI 会自动找出正确的位置 另一个例子是首要摆放 让一个条目能在你的 app 中突出显示 正如你在 iPad
和 macOS 中看到的那样 工具栏条目可以拥有位置摆放 你能对条目的放置 拥有更多设计上的控制 特别是在小尺寸类中 把条目放在底部工具栏是很常见的 bottomBar 摆放让你能指明这一点 在这些例子中 你可能已经注意到 SwiftUI 使用了一个新的标签视图 咱们来仔细看一看 这是标题和图标的组合表示法 可用于标记 UI 元素 这里有一个字符串 用作其标题 和系统图像的名称或 SF Symbol 的 本地化密钥 今年 不仅 macOS 上可以使用符号 还有数百个新符号可供你的 app 使用 “SF Symbol 2.0” 的演讲更详细地介绍了 今年对符号做的所有新改进 标签的这种构造实际上 对它的完整形式带来了便利 它能用任何视图来显示标题和图标 这种力量来自于 为该标题和图标准备的语义 让它们能基于使用位置得到妥善的处理 说回我们的工具栏示例 在工具栏的上下文中 默认情况下 它只是作为按钮标签显示的图标 和辅助功能用途的标题
这种行为从工具栏 扩展到上下文菜单 再到列表 这个列表包含多行标签 无论图像大小如何 标题都是完美对齐的 而且使用不同的动态类型大小时 Lable 的功能真的很出色 这显示了默认的大尺寸类别的布局 当它变为超大图时 图标和标题都会自动更新 包括很好地回流文本和增加列表行 在更大的辅助功能尺寸上 甚至会发生进一步的专门化
在这些情况下 标签更新了环绕在图标周围的文本 以最大限度地增加可见文本的数量 凭借工具栏这种具有了清晰的 只使用图标的标记元素样式的上下文 为这些元素提供额外的帮助或上下文 就变得前所未有地重要
有了新的 Help 修改器 你就可以加上这些描述 说明控件将具有什么效果 这在 macOS 上会显示为工具建议
真正酷的是 这个修改器可以在所有平台上使用 它还提供了一个辅助功能提示 为你在全平台的 app 提供了更好的旁白体验 在这里 我们可以看到手机上的 app 对同一工具栏条目的类似体验 进度按钮 记录新的进度项
我们的 SwiftUI 声明可以很自然地 为每个人改善我们的应用体验 这太酷了
另一种为用户与控件交互 带来更多灵活性和功能的新方法 是使用快捷指令修改器 这些通常用于场景指令 因为能够在 iPad 上 通过键盘快捷指令访问 以及如 Matt 之前所展示的 在 macOS 上通过主菜单访问 都是非常重要的 然而 快捷指令也可以用于 屏幕上显示的其他控件 比如创建具有退出键和回车键快捷指令的 取消和默认操作按钮
从键盘到电视遥控器 再到 watchOS 的数字表冠 焦点推动着这些间接输入方法 在你的 app 中路由的方式 使用新的默认焦点支持 你的 app 现在可以控制 焦点在屏幕上的起始位置 以及这种默认值 如何随你的 app 状态而改变 在“适用于 tvOS 的 SwiftUI”讲座中 我们详细介绍了如何使用新的支持 以及如何使用 SwiftUI 制作一款优秀的 tvOS app
最后一个重点是 现在有两个新控件能在你的 app 中使用 第一个是进度视图 随着时间的推移 这些可以用来显示确定和不确定的进度 有线状和圆形风格的进度视图 后者支持每个人都喜欢的旋转风格 作为一种不确定进度的显示
一个类似的新控件是仪表盘 仪表盘用于指示相对于某些总容量的 一个值的水平 这里有一个圆形的 watchOS 仪表盘 用来测量我家花园土壤的酸度 仪表盘有附加的可选定制选项 西红柿太难伺候了 以至于我真的很想速览确切的 pH 值 所以我可以添加一个 currentValueLabel 标签来显示它
仪表盘还可以有最小值和最大值标签 在某些情况下 这些可能是图像图标 但这里我要把 pH 值显示为文本
这个代码片段还强调了 Swift 中新的多个尾随闭包语法 它允许仪表盘的表达随着复杂性的增加 而自然地加长 在我的 app 支持的所有平台上 有一种新的、富有表现力的方式 来创建工具栏 这真的很好 而且这些新的方法可以真正地精雕细琢 工具栏内外控件的行为
接下来让我们看看使用 SwiftUI 创造沉浸式和有趣体验的新方法 macOS Big Sur 对通知中心 和菜单栏的新控制中心 进行了华丽的改造 都是使用 SwiftUI 构建的 控制中心在其不同的模块中 设有这些平滑的动画 使用的是你可以在自己的 app 中 应用的新功能
我在这构建了一个用户界面的小原型 来收集我最喜欢的相簿 它由一个滚动的相簿网格 和一行选定的相簿组成 在选择时 比起直接跳到那一行 我更希望相簿从网格流畅地过渡 使用匹配的几何效果 真的很简单 我可以对网格中的相簿和选定的相簿行 应用 matchedGeometryEffect 修改器 将相簿的标识符 用作该标示符来连接两个视图 以及这些标识符所对应的名称空间 在本例中 它是与所含视图关联的名称空间
创造这种效果需要的就是这些了 当相簿被从一个区域移除 然后插入另一个区域时 SwiftUI 会自动插入它们的帧 作为一个无缝过渡
另一个神奇的新工具 是 ContainerRelativeShape 这是一个新的形状类型 它将采用 与最近包含的形状类似的路径 可以在我们最喜爱的 相簿的小组件中看到效果 相簿插图上的剪切图形状 自动呈现出与小组件形状一致的 圆角形状 从而与之完美匹配 我们可以感受到这种操作多么酷炫 可以通过改变内边距 来有效地改变外部容器形状的偏移量 因此 使用 ContainerRelativeShape 对视图进行剪切时效果拔群
它会自动保持基于偏移量的同心度
还有一些其他的增强功能 来改进与文本相关的元素的体验 自定义字体将随着动态类型的变化 来自动缩放 而且 既然图像可以嵌入到文本中 那它们将作为文本的一个统一的部分 包括会对动态类型作出响应 对于任何自定义的非文本度量 比如布局 都有一个新的缩放度量属性包装器 它可以根据当前动态类型大小 自动缩放一些基本值
总之 创建响应性好 在更大的辅助功能尺寸中反应良好的 自定义布局 因此就变得十分简单了 会有另一个讲座来深入介绍这个专题 以及可以让你的 app 熠熠生辉的 其他高阶字体和排版功能 (用户界面排版功能详情) 好的 这是一些用于构建 创造性和响应性自定义视图的新工具 不过 对 app 样式的改进没有止步于此 即使在使用系统控件 你也可以自行定义 让它们在你的 app 上令人感到惬意 并通过使用自定义强调色 来让你的 app 脱颖而出 今年的新功能是在 macOS 上定制强调色 并支持在 Xcode 12 的资产目录中 直接定制强调色 这可以让你轻松地为你的 app 支持的 所有平台指定颜色
这适合在你的 app 中 应用一个基础主题色 但是在一些情况中 你可能想要特别定制 单个控件的颜色
iPadOS 和 macOS 上的网络图标 默认遵循 app 的强调色 凭借新的 listItemTint 修改器 你可以定制每个条目 甚至整个分区的图标的颜色
同样的效果也适用于 macOS 侧边栏 这些修改器也会对系统强调色的变化 作出适当的反应
同一个修改器也适用于 watchOS 它被用来给标准的表盘背景上色 我们也将这种颜色支持引入了其他控件
现在使用新的样式定制 就能对按钮和开关这种控件明确着色了 开关底色是自定义的 以遵循整体的主题强调色 而不是默认的绿色 所有这些新的视图和交互 将在你的 app 中 提供更加精致和有趣的体验 最后一个重点 让我们看看 app 集成和利用 系统提供的功能和服务的新方法
今年 SwiftUI 提供了一个一流的 API 用于打开 URL 可以在所有平台上使用
其中一种形式在新的 Link 视图中 它获取要打开的 URL 和链接的标签
它能达到你所期望的效果 使用该标签创建一个可视化元素 并使用默认网页浏览器打开该 URL
除此之外 它还可以打开 直连其他 app 的通用链接 在这个例子中 是打开新闻
链接甚至可以在小组件中运作 它们甚至可以直接回连主 app 中的内容
在 app 的上下文中 有些情况下 URL 需要通过编程的方式来打开 对于这些高阶案例 环境中还有一个 OpenURL 操作 可以在可选的完成句柄中 使用 URL 调用该操作以打开
因为它是在一个特定视图的上下文中 所以 SwiftUI 会自动打开 与它的所属窗口关联的 URL 在 iPadOS 13 的更新中 SwiftUI 获得了支持 让你的 app 既可以被拖动到其他 app 中 也可以接收来自这些 app 的投放 让你的 iPad app 更强大 更完整
在 iOS 14 和 macOS Big Sur 中 这个 API 构建在一个新的框架之上 该框架使用新的 Uniform Type Identifiers 框架 为被拖动的内容提供更强的类型化标识符
这在 SwiftUI 中得到了广泛的应用 让你得以在 app 中充分利用它的特性 从 app 的自定义导出或导入类型 扩展到自省类型 例如 获取像样的描述 或验证其一致性
“SwiftUI 中基于文档的 app”讲座 拥有更多详细内容 例如导入和导出类型之间的区别 Apple 的网站中 也有一些优质的说明文档
让你的 app 与其他服务整合的最后示例 是“通过 Apple 登录”按钮 这也是由 AuthenticationServices 提供的一流的 SwiftUI API 可以在每个平台上使用 厉害的是 只需同时导入 AuthenticationServices 和 SwiftUI 就能获得这些新的 API 不需要新的导入或框架 这只是众多现在提供 SwiftUI 视图和修改器的 Apple 框架中的一个例子 从视频播放器到地图 再到 app 片段覆盖 将这些先进的功能引入你的 SwiftUI app 变得更加容易
其中许多都完全是多平台可用的 包括能在 watchOS 上直接运行 这意味着当你学会 如何为一个平台使用这些框架时 你就能将其应用到所有平台 以上就是对 app 集成和利用 SwiftUI 现有各种系统特性的 一些新方法的快速总结 我们已经介绍了很多新特性和 API 正如 Matt 在开头提到的 我们难以面面俱到 但作为最后一个说明 在整个演讲中 我们遇到了几个例子 我们的 app 的 SwiftUI 代码 通过语言本身的改进而变得更好 今年的“Swift 中的创新” 详细介绍了 Swift 中所有了不起的变化 它提供了更多的语法改进示例 比如构造器推导 和支持在构造器内部 if let 中切换
编译器现在有了更好的诊断功能 可以帮助更快查明代码中的构建错误 最后是提高了性能 比如缩小了 SwiftUI app 的代码尺寸 加快了代码完成速度
这些都是让使用 SwiftUI 变得更有趣的东西 我们很高兴今年能与大家分享这一切 最后要感谢大家 感谢你们 让我们看到圈子里这股兴奋和热情 感谢你们关于反馈助手的报告 还有在社交媒体上的评论 论坛上的讨论 这么多天的教程 以及人们构造的所有精彩的原型和探索 我们为自己见到的兴奋感到震撼 并且真心期待即将见到的未来
-
-
1:26 - Hello World
@main struct HelloWorld: App { var body: some Scene { WindowGroup { Text("Hello, world!").padding() } } }
-
1:56 - Book Club app
@main struct BookClubApp: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { NavigationView { List(store.books) { book in Text(book.title) } .navigationTitle("Currently Reading") } } } class ReadingListStore: ObservableObject { init() {} var books = [ Book(title: "Book #1", author: "Author #1"), Book(title: "Book #2", author: "Author #2"), Book(title: "Book #3", author: "Author #3") ] } struct Book: Identifiable { let id = UUID() let title: String let author: String }
-
4:46 - Settings
@main struct BookClubApp: App { @StateObject private var store = ReadingListStore() @SceneBuilder var body: some Scene { WindowGroup { ReadingListViewer(store: store) } #if os(macOS) Settings { BookClubSettingsView() } #endif } } struct BookClubSettingsView: View { var body: some View { Text("Add your settings UI here.") .padding() } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { NavigationView { List(store.books) { book in Text(book.title) } .navigationTitle("Currently Reading") } } } class ReadingListStore: ObservableObject { init() {} var books = [ Book(title: "Book #1", author: "Author #1"), Book(title: "Book #2", author: "Author #2"), Book(title: "Book #3", author: "Author #3") ] } struct Book: Identifiable { let id = UUID() let title: String let author: String }
-
5:10 - Document groups
import SwiftUI import UniformTypeIdentifiers @main struct ShapeEditApp: App { var body: some Scene { DocumentGroup(newDocument: ShapeDocument()) { file in DocumentView(document: file.$document) } } } struct DocumentView: View { @Binding var document: ShapeDocument var body: some View { Text(document.title) .frame(width: 300, height: 200) } } struct ShapeDocument: Codable { var title: String = "Untitled" } extension UTType { static let shapeEditDocument = UTType(exportedAs: "com.example.ShapeEdit.shapes") } extension ShapeDocument: FileDocument { static var readableContentTypes: [UTType] { [.shapeEditDocument] } init(fileWrapper: FileWrapper, contentType: UTType) throws { let data = fileWrapper.regularFileContents! self = try JSONDecoder().decode(Self.self, from: data) } func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws { let data = try JSONEncoder().encode(self) fileWrapper = FileWrapper(regularFileWithContents: data) } }
-
5:49 - Custom Commands
import SwiftUI import UniformTypeIdentifiers @main struct ShapeEditApp: App { var body: some Scene { DocumentGroup(newDocument: ShapeDocument()) { file in DocumentView(document: file.$document) } .commands { CommandMenu("Shapes") { Button("Add Shape...", action: addShape) .keyboardShortcut("N") Button("Add Text", action: addText) .keyboardShortcut("T") } } } func addShape() {} func addText() {} } struct DocumentView: View { @Binding var document: ShapeDocument var body: some View { Text(document.title) .frame(width: 300, height: 200) } } struct ShapeDocument: Codable { var title: String = "Untitled" } extension UTType { static let shapeEditDocument = UTType(exportedAs: "com.example.ShapeEdit.shapes") } extension ShapeDocument: FileDocument { static var readableContentTypes: [UTType] { [.shapeEditDocument] } init(fileWrapper: FileWrapper, contentType: UTType) throws { let data = fileWrapper.regularFileContents! self = try JSONDecoder().decode(Self.self, from: data) } func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws { let data = try JSONEncoder().encode(self) fileWrapper = FileWrapper(regularFileWithContents: data) } }
-
7:55 - Widgets
import SwiftUI import WidgetKit @main struct RecommendedAlbum: Widget { var body: some WidgetConfiguration { StaticConfiguration( kind: "RecommendedAlbum", provider: Provider(), placeholder: PlaceholderView() ) { entry in AlbumWidgetView(album: entry.album) } .configurationDisplayName("Recommended Album") .description("Your recommendation for the day.") } } struct AlbumWidgetView: View { var album: Album var body: some View { Text(album.title) } } struct PlaceholderView: View { var body: some View { Text("Placeholder View") } } struct Album { var title: String } struct Provider: TimelineProvider { struct Entry: TimelineEntry { var album: Album var date: Date } public func snapshot(with context: Context, completion: @escaping (Entry) -> ()) { let entry = Entry(album: Album(title: "Untitled"), date: Date()) completion(entry) } public func timeline(with context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [Entry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = Entry(album: Album(title: "Untitled #\(hourOffset)"), date: entryDate) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } }
-
8:31 - Complications using SwiftUI
struct CoffeeHistoryChart: View { var body: some View { VStack { ComplicationHistoryLabel { Text("Weekly Coffee") .complicationForeground() } HistoryChart() } .complicationChartFont() } } struct ComplicationHistoryLabel: View { ... } struct HistoryChart: View { ... } extension View { func complicationChartFont() -> some View { ... } }
-
9:22 - Outlines
struct OutlineContentView: View { var graphics: [Graphic] var body: some View { List(graphics, children: \.children) { graphic in GraphicRow(graphic) } .listStyle(SidebarListStyle()) } } struct Graphic: Identifiable { var id: String var name: String var icon: Image var children: [Graphic]? } struct GraphicRow: View { var graphic: Graphic init(_ graphic: Graphic) { self.graphic = graphic } var body: some View { Label { Text(graphic.name) } icon: { graphic.icon } } }
-
10:09 - Adaptive grids
struct ContentView: View { var items: [Item] var body: some View { ScrollView { LazyVGrid(columns: [GridItem(.adaptive(minimum: 176))]) { ForEach(items) { item in ItemView(item: item) } } .padding() } } } struct Item: Identifiable { var name: String var id = UUID() var icon: Image { Image(systemName: name) } var color: Color { colors[colorIndex % (colors.count - 1)] } private static var nextColorIndex: Int = 0 private var colorIndex: Int init(name: String) { self.name = name colorIndex = Self.nextColorIndex Self.nextColorIndex += 1 } } struct ItemView: View { var item: Item var body: some View { ZStack { RoundedRectangle(cornerRadius: 8, style: .continuous) .fill() .layoutPriority(1) .foregroundColor(item.color) item.icon .resizable() .aspectRatio(contentMode: .fit) .padding(.all, 16) .foregroundColor(.white) } .frame(width: 176, height: 110) } }
-
10:28 - Fixed-column grids
struct ContentView: View { var items: [Item] var body: some View { ScrollView { LazyVGrid(columns: Array(repeating: GridItem(), count: 4)]) { ForEach(items) { item in ItemView(item: item) } } .padding() } } } struct Item: Identifiable { var name: String var id = UUID() var icon: Image { Image(systemName: name) } var color: Color { colors[colorIndex % (colors.count - 1)] } private static var nextColorIndex: Int = 0 private var colorIndex: Int init(name: String) { self.name = name colorIndex = Self.nextColorIndex Self.nextColorIndex += 1 } } struct ItemView: View { var item: Item var body: some View { ZStack { RoundedRectangle(cornerRadius: 8, style: .continuous) .fill() .layoutPriority(1) .foregroundColor(item.color) item.icon .resizable() .aspectRatio(contentMode: .fit) .padding(.all, 16) .foregroundColor(.white) } .frame(width: 176, height: 110) } }
-
10:38 - Horizontal grids
struct ContentView: View { var items: [Item] var body: some View { ScrollView(.horizontal) { LazyHGrid(rows: [GridItem(.adaptive(minimum: 110))]) { ForEach(items) { item in ItemView(item: item) } } .padding() } } } struct Item: Identifiable { var name: String var id = UUID() var icon: Image { Image(systemName: name) } var color: Color { colors[colorIndex % (colors.count - 1)] } private static var nextColorIndex: Int = 0 private var colorIndex: Int init(name: String) { self.name = name colorIndex = Self.nextColorIndex Self.nextColorIndex += 1 } } struct ItemView: View { var item: Item var body: some View { ZStack { RoundedRectangle(cornerRadius: 8, style: .continuous) .fill() .layoutPriority(1) .foregroundColor(item.color) item.icon .resizable() .aspectRatio(contentMode: .fit) .padding(.all, 16) .foregroundColor(.white) } .frame(width: 176, height: 110) } }
-
10:58 - Lazy stacks
struct WildlifeList: View { var rows: [ImageRow] var body: some View { ScrollView { LazyVStack(spacing: 2) { ForEach(rows) { row in switch row.content { case let .singleImage(image): SingleImageLayout(image: image) case let .imageGroup(images): ImageGroupLayout(images: images) case let .imageRow(images): ImageRowLayout(images: images) } } } } } }
-
12:24 - Toolbar modifier
struct ContentView: View { var body: some View { List { Text("Book List") } .toolbar { Button(action: recordProgress) { Label("Record Progress", systemImage: "book.circle") } } } private func recordProgress() {} }
-
12:40 - ToolbarItem
struct ContentView: View { var body: some View { List { Text("Book List") } .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: recordProgress) { Label("Record Progress", systemImage: "book.circle") } } } } private func recordProgress() {} }
-
12:47 - Confirmation and cancellation toolbar placements
struct ContentView: View { var body: some View { Form { Slider(value: .constant(0.39)) } .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Save", action: saveProgress) } ToolbarItem(placement: .cancellationAction) { Button("Cancel", action: dismissSheet) } } } private func saveProgress() {} private func dismissSheet() {} }
-
13:00 - Principal toolbar placement
struct ContentView: View { enum ViewMode { case details case notes } @State private var viewMode: ViewMode = .details var body: some View { List { Text("Book Detail") } .toolbar { ToolbarItem(placement: .principal) { Picker("View", selection: $viewMode) { Text("Details").tag(ViewMode.details) Text("Notes").tag(ViewMode.notes) } } } } }
-
13:17 - Bottom bar toolbar placement
struct ContentView: View { var body: some View { List { Text("Book Detail") } .toolbar { ToolbarItem { Button(action: recordProgress) { Label("Progress", systemImage: "book.circle") } } ToolbarItem(placement: .bottomBar) { Button(action: shareBook) { Label("Share", systemImage: "square.and.arrow.up") } } } } private func recordProgress() {} private func shareBook() {} }
-
13:38 - Label
Label("Progress", systemImage: "book.circle")
-
14:06 - Label expanded form
Label { Text("Progress") } icon: { Image(systemName: "book.circle") }
-
14:24 - Bottom bar toolbar placement
struct ContentView: View { var body: some View { List { Text("Book Detail") } .toolbar { ToolbarItem { Button(action: recordProgress) { Label("Progress", systemImage: "book.circle") } } ToolbarItem(placement: .bottomBar) { Button(action: shareBook) { Label("Share", systemImage: "square.and.arrow.up") } } } } private func recordProgress() {} private func shareBook() {} }
-
14:36 - Context menu Labels
struct ContentView: View { var body: some View { List { Text("Book List Row") .contextMenu { Button(action: recordProgress) { Label("Progress", systemImage: "book.circle") } Button(action: addToFavorites) { Label("Add to Favorites", systemImage: "heart") } Button(action: shareBook) { Label("Share", systemImage: "square.and.arrow.up") } } } } private func recordProgress() {} private func addToFavorites() {} private func shareBook() {} }
-
14:39 - List Labels
struct ContentView: View { var body: some View { List { Group { Label("Introducing SwiftUI", systemImage: "hand.wave") Label("SwiftUI Essentials", systemImage: "studentdesk") Label("Data Essentials in SwiftUI", systemImage: "flowchart") Label("App Essentials in SwiftUI", systemImage: "macwindow.on.rectangle") } Group { Label("Build Document-based apps in SwiftUI", systemImage: "doc") Label("Stacks, Grids, and Outlines", systemImage: "list.bullet.rectangle") Label("Building Custom Views in SwiftUI", systemImage: "sparkles") Label("Build SwiftUI Apps for tvOS", systemImage: "tv") Label("Build SwiftUI Views for Widgets", systemImage: "square.grid.2x2.fill") Label("Create Complications for Apple Watch", systemImage: "gauge") Label("SwiftUI on All Devices", systemImage: "laptopcomputer.and.iphone") Label("Integrating SwiftUI", systemImage: "rectangle.connected.to.line.below") } } } }
-
15:28 - Help modifier
struct ContentView: View { var body: some View { Button(action: recordProgress) { Label("Progress", systemImage: "book.circle") } .help("Record new progress entry") } private func recordProgress() {} }
-
16:12 - Keyboard shortcut modifier
@main struct BookClubApp: App { var body: some Scene { WindowGroup { List { Text("Reading List Viewer") } } .commands { Button("Previous Book", action: selectPrevious) .keyboardShortcut("[") Button("Next Book", action: selectNext) .keyboardShortcut("]") } } private func selectPreviousBook() {} private func selectNextBook() {} }
-
16:28 - Cancel and default action keyboard shortcuts
struct ContentView: View { var body: some View { HStack { Button("Cancel", action: dismissSheet) .keyboardShortcut(.cancelAction) Button("Save", action: saveProgress) .keyboardShortcut(.defaultAction) } } private func dismissSheet() {} private func saveProgress() {} }
-
17:08 - ProgressView
struct ContentView: View { var percentComplete: Double var body: some View { ProgressView("Downloading Photo", value: percentComplete) } }
-
17:19 - Circular ProgressView
struct ContentView: View { var percentComplete: Double var body: some View { ProgressView("Downloading Photo", value: percentComplete) .progressViewStyle(CircularProgressViewStyle()) } }
-
17:25 - Activity indicator ProgressView
struct ContentView: View { var body: some View { ProgressView() } }
-
17:32 - Gauge
struct ContentView: View { var acidity: Double var body: some View { Gauge(value: acidity, in: 3...10) { Label("Soil Acidity", systemImage: "drop.fill") .foregroundColor(.green) } } }
-
17:52 - Gauge with current value label
struct ContentView: View { var acidity: Double var body: some View { Gauge(value: acidity, in: 3...10) { Label("Soil Acidity", systemImage: "drop.fill") .foregroundColor(.green) } currentValueLabel: { Text("\(acidity, specifier: "%.1f")") } } }
-
18:00 - Gauge with minimum and maximum value labels
struct ContentView: View { var acidity: Double var body: some View { Gauge(value: acidity, in: 3...10) { Label("Soil Acidity", systemImage: "drop.fill") .foregroundColor(.green) } currentValueLabel: { Text("\(acidity, specifier: "%.1f")") } minimumValueLabel: { Text("3") } maximumValueLabel: { Text("10") } } }
-
18:57 - Initial Album Picker
struct ContentView: View { @State private var selectedAlbumIDs: Set<Album.ID> = [] var body: some View { VStack(spacing: 0) { ScrollView { albumGrid.padding(.horizontal) } Divider().zIndex(-1) selectedAlbumRow .frame(height: AlbumCell.albumSize) .padding(.top, 8) } .buttonStyle(PlainButtonStyle()) } private var albumGrid: some View { LazyVGrid(columns: [GridItem(.adaptive(minimum: AlbumCell.albumSize))], spacing: 8) { ForEach(unselectedAlbums) { album in Button(action: { select(album) }) { AlbumCell(album) } } } } private var selectedAlbumRow: some View { HStack { ForEach(selectedAlbums) { album in AlbumCell(album) } } } private var unselectedAlbums: [Album] { Album.allAlbums.filter { !selectedAlbumIDs.contains($0.id) } } private var selectedAlbums: [Album] { Album.allAlbums.filter { selectedAlbumIDs.contains($0.id) } } private func select(_ album: Album) { withAnimation(.spring(response: 0.5)) { _ = selectedAlbumIDs.insert(album.id) } } } struct AlbumCell: View { static let albumSize: CGFloat = 100 var album: Album init(_ album: Album) { self.album = album } var body: some View { album.image .frame(width: AlbumCell.albumSize, height: AlbumCell.albumSize) .background(Color.pink) .cornerRadius(6.0) } } struct Album: Identifiable { static let allAlbums: [Album] = [ .init(name: "Sample", image: Image(systemName: "music.note")), .init(name: "Sample 2", image: Image(systemName: "music.note.list")), .init(name: "Sample 3", image: Image(systemName: "music.quarternote.3")), .init(name: "Sample 4", image: Image(systemName: "music.mic")), .init(name: "Sample 5", image: Image(systemName: "music.note.house")), .init(name: "Sample 6", image: Image(systemName: "tv.music.note")) ] var name: String var image: Image var id: String { name } }
-
19:17 - Matched geometry effect Album Picker
struct ContentView: View { @Namespace private var namespace @State private var selectedAlbumIDs: Set<Album.ID> = [] var body: some View { VStack(spacing: 0) { ScrollView { albumGrid.padding(.horizontal) } Divider().zIndex(-1) selectedAlbumRow .frame(height: AlbumCell.albumSize) .padding(.top, 8) } .buttonStyle(PlainButtonStyle()) } private var albumGrid: some View { LazyVGrid(columns: [GridItem(.adaptive(minimum: AlbumCell.albumSize))], spacing: 8) { ForEach(unselectedAlbums) { album in Button(action: { select(album) }) { AlbumCell(album) } .matchedGeometryEffect(id: album.id, in: namespace) } } } private var selectedAlbumRow: some View { HStack { ForEach(selectedAlbums) { album in AlbumCell(album) .matchedGeometryEffect(id: album.id, in: namespace) } } } private var unselectedAlbums: [Album] { Album.allAlbums.filter { !selectedAlbumIDs.contains($0.id) } } private var selectedAlbums: [Album] { Album.allAlbums.filter { selectedAlbumIDs.contains($0.id) } } private func select(_ album: Album) { withAnimation(.spring(response: 0.5)) { _ = selectedAlbumIDs.insert(album.id) } } } struct AlbumCell: View { static let albumSize: CGFloat = 100 var album: Album init(_ album: Album) { self.album = album } var body: some View { album.image .frame(width: AlbumCell.albumSize, height: AlbumCell.albumSize) .background(Color.pink) .cornerRadius(6.0) } } struct Album: Identifiable { static let allAlbums: [Album] = [ .init(name: "Sample", image: Image(systemName: "music.note")), .init(name: "Sample 2", image: Image(systemName: "music.note.list")), .init(name: "Sample 3", image: Image(systemName: "music.quarternote.3")), .init(name: "Sample 4", image: Image(systemName: "music.mic")), .init(name: "Sample 5", image: Image(systemName: "music.note.house")), .init(name: "Sample 6", image: Image(systemName: "tv.music.note")) ] var name: String var image: Image var id: String { name } }
-
19:53 - Container Relative Shape
struct AlbumWidgetView: View { var album: Album var body: some View { album.image .clipShape(ContainerRelativeShape()) .padding() } } struct Album { var name: String var artist: String var image: Image }
-
20:34 - Dynamic Type scaling
struct ContentView: View { var album: Album @ScaledMetric private var padding: CGFloat = 10 var body: some View { VStack { Text(album.name) .font(.custom("AvenirNext-Bold", size: 30)) Text("\(Image(systemName: "music.mic")) \(album.artist)") .font(.custom("AvenirNext-Bold", size: 17)) } .padding(padding) .background(RoundedRectangle(cornerRadius: 16, style: .continuous).fill(Color.purple)) } } struct Album { var name: String var artist: String var image: Image }
-
22:08 - Initial Sidebar List
struct ContentView: View { var body: some View { NavigationView { List { Label("Menu", systemImage: "list.bullet") Label("Favorites", systemImage: "heart") Label("Rewards", systemImage: "seal") Section(header: Text("Recipes")) { ForEach(1..<4) { Label("Recipes \($0)", systemImage: "book.closed") } } } .listStyle(SidebarListStyle()) } } }
-
22:17 - List Item Tint in Sidebars
struct ContentView: View { var body: some View { NavigationView { List { Label("Menu", systemImage: "list.bullet") Label("Favorites", systemImage: "heart") .listItemTint(.red) Label("Rewards", systemImage: "seal") .listItemTint(.purple) Section(header: Text("Recipes")) { ForEach(1..<4) { Label("Recipes \($0)", systemImage: "book.closed") } } .listItemTint(.monochrome) } .listStyle(SidebarListStyle()) } } }
-
22:33 - List Item Tint on watchOS
struct ContentView: View { var body: some View { NavigationView { List { Label("Menu", systemImage: "list.bullet") Label("Favorites", systemImage: "heart") .listItemTint(.red) Label("Rewards", systemImage: "seal") .listItemTint(.purple) Section(header: Text("Recipes")) { ForEach(1..<4) { Label("Recipes \($0)", systemImage: "book.closed") } } .listItemTint(.monochrome) } } } }
-
22:46 - SwitchToggleStyle tint
struct ContentView: View { @State var order = Order() var body: some View { Toggle("Send notification when ready", isOn: $order.notifyWhenReady) .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } struct Order { var notifyWhenReady = true }
-
23:15 - Link
let appleURL = URL(string: "https://developer.apple.com/tutorials/swiftui/")! let wwdcAnnouncementURL = URL(string: "https://apple.news/AjriX1CWUT-OfjXu_R4QsnA")! struct ContentView: View { var body: some View { Form { Section { Link(destination: apple) { Label("SwiftUI Tutorials", systemImage: "swift") } Link(destination: wwdcAnnouncementURL) { Label("WWDC 2020 Announcement", systemImage: "chevron.left.slash.chevron.right") } } } } }
-
23:56 - OpenURL Environment Action
let customPublisher = NotificationCenter.default.publisher(for: .init("CustomURLRequestNotification")) let apple = URL(string: "https://developer.apple.com/tutorials/swiftui/")! struct ContentView: View { @Environment(\.openURL) private var openURL var body: some View { Text("OpenURL Environment Action") .onReceive(customPublisher) { output in if output.userInfo!["shouldOpenURL"] as! Bool { openURL(apple) } } } }
-
24:44 - Uniform Type Identifiers
import UniformTypeIdentifiers extension UTType { static let myFileFormat = UTType(exportedAs: "com.example.myfileformat") } func introspecContentType(_ fileURL: URL) throws { // Get this file's content type. let resourceValues = try fileURL.resourceValues(forKeys: [.contentTypeKey]) if let type = resourceValues.contentType { // Get the human presentable description of the type. let description = type.localizedDescription if type.conforms(to: .myFileFormat) { // The file is our app’s format. } else if type.conforms(to: .image) { // The file is an image. } } }
-
25:16 - Sign in with Apple Button
import AuthenticationServices import SwiftUI struct ContentView: View { var body: some View { SignInWithAppleButton( .signUp, onRequest: handleRequest, onCompletion: handleCompletion ) .signInWithAppleButtonStyle(.black) } private func handleRequest(request: ASAuthorizationAppleIDRequest) {} private func handleCompletion(result: Result<ASAuthorization, Error>) {} }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。