大多数浏览器和
Developer App 均支持流媒体播放。
-
SwiftUI 基础知识
和我们一起简单了解一下 SwiftUI — Apple 的声明式用户界面框架。了解利用 SwiftUI 构建 App 所涉及的基本概念,例如视图、状态变量以及布局。探索丰富多样的 API,以帮助你打造全方位 App 体验,并设计独一无二的自定组件。无论你是刚接触 SwiftUI,还是经验丰富的开发者,你都将了解到如何充分利用 SwiftUI 的强大功能来打造出色的 App。
章节
- 0:00 - Introduction
- 1:34 - Fundamentals of views
- 13:06 - Bulit-in capability
- 17:36 - Across all platforms
- 20:30 - SDK interoperability
资源
相关视频
WWDC24
WWDC20
-
下载
大家好 我叫 Taylor 欢迎学习“SwiftUI 基础知识” SwiftUI 是 Apple 的 声明式用户界面框架 用于开发适合 各个 Apple 平台的 App 这个框架已在 Apple 内部广泛采用 既用于全新 App 的开发 也逐步应用于现有 App 当你在开发新的 App 或新功能时 SwiftUI 是非常理想的工具 原因有几个 SwiftUI 提供了 丰富多样的功能 让你的 App 能够 充分利用所运行的设备 能在 Apple 的平台上提供 原生体验和丰富的交互功能 而且添加这些功能 需要编写的代码量更少 因此你能更快地 从原型过渡到生产环境 让你更专注于打造 App 的独特之处 SwiftUI 支持渐进式采用策略 这意味着你可以 根据需要灵活利用 我们并不要求整个 App 必须完全基于 SwiftUI 才能受益于它 这些特质让任何人 都能轻松学习如何 用 SwiftUI 构建 App 而理解 SwiftUI 如何实现这些特质 可以帮助你了解 如何最有效地利用这些特质 我将从最基础的内容入手 介绍一下 视图的工作原理 之后 我将着重介绍 SwiftUI 中内置的 一些功能 以及这些功能在 各种 Apple 平台上运作的方式 最后 我将探讨 SwiftUI 与其他框架集成的能力
但在开始之前 我想分享关于 SwiftUI 和背后团队的趣闻: 我们都非常喜欢自己的宠物 常常争论 哪种宠物最棒 为了公正解决这一问题 我决定采取最客观的方式 看看哪种宠物能表演最棒的技巧 这无疑是一场高手间的较量
在这个视频中 我将使用 SwiftUI 构建一款 App 用于记录我们的宠物、 它们学会的技巧及在竞争中的排名 但每个项目都有个起点 而在 SwiftUI 中 一切始于视图 视图是用户界面的 基本构建块 对于你在 SwiftUI 中 所做的一切至关重要 屏幕上显示的每个像素 都以某种方式由视图定义 SwiftUI 视图之所以特别 主要归功于三大特性: 声明式 组合性 和基于状态驱动 视图通过声明式方式表达
你只需描述希望 在用户界面中呈现什么样的视图 SwiftUI 就会自动生成相应效果 你可以创建文本、 含 SF Symbols 的图像 以及控件 比如按钮
这个代码创建了一个水平堆栈 其中包含一个由图标和标题 组合而成的标签 一个空白区域及末尾的文本 这种声明式语法 同样适用于其他容器 比如可滚动列表
当给定一个宠物集合时 此列表会为每种宠物 创建一个水平堆栈 鉴于不是每只宠物都叫 Whiskers 我将更新堆栈中的视图 改为使用每只宠物的属性
从始至终 我都不需要描述 生成这个 UI 所需的操作 比如向列表中 添加或删除行
这正是声明式编程 和命令式编程的区别所在 如果你 曾教过宠物做把戏 你对命令式指令应该相当熟悉
就像命令式编程一样 我可以一步步指示 这只叫 Rufus 的狗狗击出本垒打: Rufus 过来 Rufus 拿球棒 Rufus 站到本垒板上 如此这般 逐一描述流程的每个步骤
相比之下 声明式宠物技巧 则是描述 你想要达成的结果 并让事先已训练好的小狗 为你完成 你只需说出你想要的: 看到 Rufus 打出全垒打 而且你可以 自定这个技巧的某些方面 比如让 Rufus 穿上定制的球服 声明式编程和命令式编程 并不互斥 声明式代码让你 专注于预期结果 而非达成目标的具体步骤 而命令式代码 则非常适合需要更改状态 或在没有现成的 声明式组件时使用 而 SwiftUI 支持这两种代码 按钮就是一个很好的例子 按钮以声明方式添加到用户界面 声明的一部分 是轻点时执行的操作 这个操作使用命令式代码 来实现状态的改变: 比如在这个示例中 向列表中添加新的宠物
SwiftUI 的视图是 对 UI 当前状态的描述 它们不是随时间推移 接收命令式指令的长期对象实例 因此 SwiftUI 视图是值类型 使用结构而不是类进行定义
SwiftUI 接收这些描述 并创建一个高效数据结构 来表示它们 它在后台维护这个数据结构 用于生成不同的输出 例如:屏幕上显示的内容 各种手势和视图的交互元素 及相关辅助功能呈现方式 由于视图 是声明式的描述 将一个视图拆分为多个视图 不会影响 App 的性能 在追求最佳性能的同时 你无需在代码组织方式上 做出让步 完全可以按自己的意愿去组织代码
组合在整个 SwiftUI 中广泛使用 是每个用户界面的 重要组成部分 我先前构建的 HStack 是一个用于布局的 容器视图 它将子项放置在水平堆栈中 在 SwiftUI 中重新排列 和试验容器视图 非常简单 代码本身类似于 它创建的视图层次结构
水平堆栈包含三个视图: 图像、垂直堆栈和空白区域 垂直堆栈自身又包含两个视图: 标签和文本
这种语法使用视图构建器闭包 来声明容器的子项 在这个示例中 我正在使用 HStack 的构造器 它使用了 ViewBuilder 内容参数 这是 SwiftUI 中 所有容器视图都使用的模式
组合在另一个 SwiftUI 模式中 发挥着重要的作用 即视图修饰符 视图修饰符把修改应用于基本视图 并可以更改这个视图的任何方面 我将用 Whiskers 的可爱照片来演示 首先将它裁剪成圆形 添加阴影 并在上面叠加绿色边框 那是它最喜欢的颜色
从语法上讲 这与容器视图有很大不同 但最终也会形成 类似的层次结构 效果的层次结构和顺序是 根据修饰符的确切顺序确定的 将修饰符串联起来 可以清晰地展示出结果是如何产生的 以及如何自定这个结果 所有这些都采用易于阅读的语法实现
视图层次结构可以封装到 自定视图和视图修饰符中 自定视图 遵照 View 协议 并使用 body 属性 来返回它所代表的视图 从 body 返回的视图 使用我到目前为止展示的 相同的视图构建语法 支持相同的组合特性和快速迭代 你可以创建其他视图属性 来帮助你根据需要组织代码 我已将头像结构重构为 一个私有的视图属性
通过这些循序渐进的步骤 我可以持续迭代 并构建出 完全符合需求的行视图
自定视图会接收改变 body 生成方式的输入 我已经为这一行将要表示的宠物 添加了一个属性 并在从 body 返回的视图 中使用了这个属性
通过这一改动 我可以重复使用相同的视图 来展示关于 Whiskers 以及 Roofus 和 Bubbles 的信息
自定视图与任何其他视图的 使用方式一样 在这里 我在列表中用 自定视图为每只宠物创建视图
列表非常有效地证明了 视图组合的强大 这个 List 构造器 使用集合参数 这为创建 ForEach 视图 提供了便利 ForEach 为集合中的 每个元素生成视图 并将这些视图提供给相应容器 这种基于视图的 List 构造器 支持创建 更高级的结构 比如将多个集合的数据 分到不同板块中 一个板块用于我的宠物 另一个用于他人的宠物
列表也可以借由视图修饰符 来实现定制化 例如 为每一行添加轻扫操作
通过更多容器和修饰符的组合 我可以循序渐进地 构建整个 App
SwiftUI 中的视图 的第三个特点 是基于状态驱动 当视图的状态随时间发生变化时 SwiftUI 会自动保持 UI 的最新状态 消除样板代码和更新错误 SwiftUI 在后台维护 用户界面的呈现 随着数据的变化 新的视图值被创建 并传给 SwiftUI SwiftUI 会根据这些值来确定 如何更新它的输出 我的 App 现在列出了 宠物及技巧 但宠物比赛中 最重要的部分 是给我认为 掌握了最佳技巧的宠物颁发奖励 这是 Sheldon 它想获得的奖励是草莓 在为每行添加了滑动操作后 我已经做好了 充分的准备
当我轻点奖励按钮时 会调用响应操作 这个操作会修改关联的宠物对象 并将 hasAward 更改为 true
SwiftUI 会跟踪 所有依赖于这个宠物的任何视图 比如行视图
这里引用了相应的宠物 在 body 中读取 宠物是否获得奖励的信息 从而建立依赖项
随着宠物状态更新 SwiftUI 将再次调用这个视图的 body
它现在返回的结果中 会包含一张图像 以显示 Sheldon 获得的奖励
SwiftUI 根据这个结果 更新输出 从而在屏幕上显示新图像
视图在它的 body 中 使用的任何数据 都是这个视图的依赖项 在我的 App 中 我创建了一个可观察的宠物类 SwiftUI 能针对视图 body 中 所使用的特定属性 创建依赖项 SwiftUI 提供了几种用于 管理状态的工具 另外两个重要工具是 State 和 Binding State 会为视图创建 新的内部数据源 使用 @State 标记视图属性后 SwiftUI 会管理这个属性的存储 并确保视图 能够读取和写入这个属性
Binding 则创建对其他一些 视图状态的双向引用
我编写了另一个 利用这些特性的视图 这个视图 让我可以给宠物的技巧打分 它使用 State 来跟踪当前评分 并持续更新评分 这个值会显示在 中间的显眼位置 并有两个按钮来 增加或减少值
SwiftUI 会在后台 维护这个状态的值
与前面的示例类似 轻点按钮就能调用响应操作 这一次 轻点操作增加了 视图内部的 State 值
SwiftUI 检测这一变化 并在 RatingView 上调用 body 这会返回一个新的文本值 更新后的结果随后显示在屏幕上
我将重点放在 body 中的视图部分 因为状态变化发生在这里
目前 更改会立即生效 没有伴随动画效果 SwiftUI 中的动画是 基于我之前提到的 数据驱动更新机制构建的
当我用 withAnimation 包裹 这个状态变化后 生成的视图更新 会带有默认的动画效果
SwiftUI 为文本 应用了默认的 淡入淡出过渡 但我也可以自定过渡效果
在这个示例中 数字文本内容过渡 是理想的选项
通过结合状态和动画效果 我构建了一个封装视图组件 它具备了我想要的互动效果 最终 我将在 App 的其余部分 都使用这个视图
这里是另一个视图 名为 RatingContainerView 它的 body 中包括 Gauge 和 RatingView
目前 每个视图都有各自的状态 作为自身的 评分来源 然而 这意味着当评分视图 自身状态增加时 容器视图的状态 和 Gauge 都不会随之改变
我已经更新了 RatingView 让它接受 Binding 作为输入 以便它的容器视图可以 提供双向引用
现在 容器视图的状态 成为了唯一的数据源 不仅向 Gauge 提供值 还为 RatingView 提供 Binding 这样三者就实现了同步更新 而且动画状态改变 也会应用于 Gauge
SwiftUI 提供了多个级别的 内置功能 为你构建 App 提供了更高的起点
我这个追踪宠物及技巧的 App 才刚刚起步 但我对目前的进展颇为满意 SwiftUI 自动提供 多个维度的自适应性
我的 App 在深色模式下效果不错 它支持多个辅助功能 比如动态类型 现在可以对它进行本地化了 比方说 我要用一种从右到左显示的 模拟语言来预览它 体验一下 希伯来语或阿拉伯语的展示效果
这是使用 Xcode 预览的好处之一 它能快速显示视图的外观 包括在不同环境中的效果 它会在你编写代码时显示效果 不需要一遍遍地运行 App
预览甚至在设备上 也是交互式的 在构建功能的同时 你可以确切了解到 正在开发的这个功能 能带来怎样的体验
SwiftUI 声明式视图的一大优势 在于自适应性 SwiftUI 提供的视图 通常描述功能意图 而不是 具体的视觉结构
之前我展示了如何用按钮等视图 组成轻扫操作 按钮就是自适应视图的 绝佳示例 它们有两个基本属性: 操作和描述操作的标签
它们可用于 许多不同的场景中 但始终承载着 所标记操作的核心功能
它们能适应不同的样式 例如无边框、带边框 或醒目显示 能自动根据不同的环境 进行调整 例如轻扫动作、菜单和表单 这个模式适用于 SwiftUI 中的所有控件 包括切换开关 切换开关有不同的样式 例如开关、复选框 和切换按钮 在不同的环境中 会以符合习惯的样式显示 用以表示 打开和关闭状态
SwiftUI 中的许多视图 都具有相同的自适应特性 利用组合来影响行为 并允许定制 某些视图修饰符 也同样如此 我最常用的示例之一是 searchable 我要将它应用于我的宠物列表
在添加 searchable 修饰符时 我描述的是 应用这个修饰符的视图 是可以进行搜索的 SwiftUI 会处理所有细节 并以符合习惯的方式实现这一功能 通过逐步采用 其他修饰符 你可以定制体验: 如添加建议、筛选范围 和关键词标记
SwiftUI 的声明式视图和自适应视图 只需几行代码即可 实现众多功能
其中包括各种控件 例如按钮、切换开关和选择器 NavigationSplitView 等容器视图 或可自定的多列表格 展示模式 例如模态窗口和检查器 以及文档中提供的 许多可供探索的其他示例
当你准备好打造 独特且个性化的用户体验时 SwiftUI 还为你准备了另一层的 API 提供更深层的控制支持 你可以打造自己的控件样式 使用 Canvas 进行高性能 命令式绘图 创建完全自定的布局 甚至直接在 SwiftUI 视图上 应用自定 Metal 着色器
在我的 App 中 记分牌就是 利用这些更深层工具 打造独特体验的理想位置 旨在重现经典的翻页板效果 我使用了动画、图形技巧 并巧妙融入了 Metal 着色器
Sheldon 在最后一个技巧中 没能完美落地 我只能给他打 7 分了 再接再励 伙计
SwiftUI 的功能 不仅限于视图 整个 App 的定义 都是基于与视图相同的原则 构建的 App 是由场景定义的 声明式结构 WindowGroup 是一种场景 它使用一种内容视图创建 以在屏幕上显示 不同的场景也可以组合
在 macOS 之类的 多窗口平台上 通过增设场景 用户能以多种方式 与 App 功能进行交互
这个模式同样适用于 构建自定小组件 小组件显示在 主屏幕和桌面上 并由视图组成 我重复使用了一些记分牌视图 来显示 Sheldon 的最新评分 SwiftUI 的功能可扩展到 它所运行的任何平台 使你能够把在一个平台上开发的成果 转换为 在其他平台上构建的原生 App 为任何 Apple 平台构建 App 时 都可以使用 SwiftUI
这能让你事半功倍: 只要使用 SwiftUI 为一个平台 构建用户界面之后 你可以水到渠成地 将这个 UI 移植到任何平台
我为 iOS 构建的 App 就是一个很好的例子
自适应视图和场景 可在任何 Apple 平台上 提供符合习惯的外观和风格 在 macOS 上 自动支持键盘导航 或创建多个窗口等功能
同样使用搜索建议 在 macOS 上会生成 标准下拉菜单 在 iOS 上会生成浮动列表
使用更深层的 API 自定制作的视图 在不同平台上 会产生相同的结果 这也是在需要时 重复使用相同视图的又一绝佳途径
我为完善记分牌动画 所做的工作 在所有平台上看起来都很棒
虽然 SwiftUI 通过这些方式 实现代码共享 但它并非编写一次就可用于所有平台 它是一套工具 你只需学习一次 就可以在任何环境 或任何 Apple 平台上使用
SwiftUI 提供一套在所有平台上 通用的高低层级组件集 但它也为每个平台提供了专门的 API
每个平台的使用方式 各不相同 因此设计方式也不一样
《人机界面指南》 介绍了组件、 模式 和平台的注意事项
NavigationSplitView 自动适应 watchOS 推送详细信息的 源列表设计
我重复使用了记分牌 这样的自定视图 但我想要针对 watchOS 做出一项更改 我希望能够使用数码表冠 来快速选择评分 而不是使用 触控或键盘
在相同的记分牌视图的基础上 我为 watchOS 额外添加了 一个修饰符: digitalCrownRotation
现在当我转动数码表冠时 它会翻到我想要的分数
当我在 Mac 上查看宠物 和它们的技巧时 我想深入了解过去的数据 并比较各种宠物的数据 我可以在不同的 场景类型中利用 macOS 灵活的窗口模型 也可以使用能充分利用 macOS 熟悉的控件库、 信息密度和精确输入的视图
我还可以将 App 移植到 visionOS 利用其他平台的视图 并添加额外的立体内容 SwiftUI 助你打造在各个平台上 都表现出色的 App 这在本质上也是渐进式的 SwiftUI 不需要你 支持多个平台 但它为你准备好了 随时扩展的起点 最后一个方面不是 SwiftUI 本身的内置功能 但它体现了 SwiftUI 与其他框架的 功能进行互操作的能力 SwiftUI 是每个平台的 SDK 自带的框架 SDK 中还包括 许多其他框架 每个框架 各自拥有强大的功能 一个 App 不可能用到所有这些框架 但你可以从提供所需技术的 框架中进行选择
SwiftUI 为所有这些功能 提供了互操作性 在许多情况下 就像将另一个视图或属性 拖放到你的 App 中一样简单
UIKit 和 AppKit 是命令式的 面向对象的用户界面框架 它们都提供与 SwiftUI 类似的 构建块 但使用不同的模式 来创建和更新视图 它们还具备存在已久的丰富功能 SwiftUI 正是在这些基础上构建的
SwiftUI 的一个核心特性 是与它们的无缝互操作性
如果想要在 SwiftUI 中使用 UIKit 或 AppKit 中的 视图或视图控制器 你可以创建可表示视图
这是一种特殊的 SwiftUI 视图协议 用于使用命令式代码 创建和更新 关联的 UIKit 或 AppKit 视图
这将生成一个可以在 SwiftUI 的 声明式视图构建器中使用的视图 可以像任何其他视图一样使用 比如在 HStack 中使用 反之亦然 如果想将 SwiftUI 视图嵌入 UIKit 或 AppKit 视图层次结构中 你可以使用 Hosting View Controller 等类 这是使用根 SwiftUI 视图创建的 可以添加到 UIKit 或 AppKit 视图控制器层次结构中 Apple 自己的 App 使用这些工具 来逐步采用 SwiftUI 无论是将 SwiftUI 引入现有 App 还是在构建全新的 SwiftUI App 和整合 Kit 视图时 这些都是可供你用来 构建出色 App 的现成工具 我们并不要求整个 App 必须完全基于 SwiftUI 才能受益于它
SDK 中的每个框架 都各自具备独特的功能 SwiftData 使你能够快速 将持久性模型 添加到 App 中 并提供了 API 用于从你的 SwiftUI 视图 连接和查询这些模型
Swift Charts 是一个 高度可定制的图表框架 构建在 SwiftUI 之上 简化了创建 美观信息可视化效果的过程
所有这些框架 都可以用来帮助你 构建出色的 App SwiftUI 是以声明式、组合性且 基于状态驱动的视图 为基础构建的 此外它还提供了 平台惯用的功能 以及与各种 SDK 的集成 所有这些都可以帮助你 集中精力打造 App 的独特之处 使用更少的代码 提供丰富多样的组件 用于打造符合习惯 且引人入胜的应用程序 同时支持渐进式采用
现在是时候 开始使用 SwiftUI 了 启动 Xcode 开始创建你的第一个 App 或开始将 SwiftUI 融入现有 App 中 查看更多关于 SwiftUI 的 精彩视频 接下来推荐观看 “Swift UI 简介”
跟随 SwiftUI 教程逐步学习 这些教程将指导你 构建不同的 App 文档中还有更多精彩内容 等待你探索
至于宠物比赛 要决定哪种宠物最棒 仅凭一个 App 怕是难以办到 目前我只能说 它们都是很棒的宠物
-
-
2:30 - Declarative views
Text("Whiskers") Image(systemName: "cat.fill") Button("Give Treat") { // Give Whiskers a treat }
-
2:43 - Declarative views: layout
HStack { Label("Whiskers", systemImage: "cat.fill") Spacer() Text("Tightrope walking") }
-
2:56 - Declarative views: list
struct ContentView: View { @State private var pets = Pet.samplePets var body: some View { List(pets) { pet in HStack { Label("Whiskers", systemImage: "cat.fill") Spacer() Text("Tightrope walking") } } } } struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String init(_ name: String, kind: Kind, trick: String) { self.name = name self.kind = kind self.trick = trick } static let samplePets = [ Pet("Whiskers", kind: .cat, trick: "Tightrope walking"), Pet("Roofus", kind: .dog, trick: "Home runs"), Pet("Bubbles", kind: .fish, trick: "100m freestyle"), Pet("Mango", kind: .bird, trick: "Basketball dunk"), Pet("Ziggy", kind: .lizard, trick: "Parkour"), Pet("Sheldon", kind: .turtle, trick: "Kickflip"), Pet("Chirpy", kind: .bug, trick: "Canon in D") ] }
-
3:07 - Declarative views: list
struct ContentView: View { @State private var pets = Pet.samplePets var body: some View { List(pets) { pet in HStack { Label(pet.name, systemImage: pet.kind.systemImage) Spacer() Text(pet.trick) } } } } struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String init(_ name: String, kind: Kind, trick: String) { self.name = name self.kind = kind self.trick = trick } static let samplePets = [ Pet("Whiskers", kind: .cat, trick: "Tightrope walking"), Pet("Roofus", kind: .dog, trick: "Home runs"), Pet("Bubbles", kind: .fish, trick: "100m freestyle"), Pet("Mango", kind: .bird, trick: "Basketball dunk"), Pet("Ziggy", kind: .lizard, trick: "Parkour"), Pet("Sheldon", kind: .turtle, trick: "Kickflip"), Pet("Chirpy", kind: .bug, trick: "Canon in D") ] }
-
4:24 - Declarative and imperative programming
struct ContentView: View { @State private var pets = Pet.samplePets var body: some View { Button("Add Pet") { pets.append(Pet("Toby", kind: .dog, trick: "WWDC Presenter")) } List(pets) { pet in HStack { Label(pet.name, systemImage: pet.kind.systemImage) Spacer() Text(pet.trick) } } } } struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String init(_ name: String, kind: Kind, trick: String) { self.name = name self.kind = kind self.trick = trick } static let samplePets = [ Pet("Whiskers", kind: .cat, trick: "Tightrope walking"), Pet("Roofus", kind: .dog, trick: "Home runs"), Pet("Bubbles", kind: .fish, trick: "100m freestyle"), Pet("Mango", kind: .bird, trick: "Basketball dunk"), Pet("Ziggy", kind: .lizard, trick: "Parkour"), Pet("Sheldon", kind: .turtle, trick: "Kickflip"), Pet("Chirpy", kind: .bug, trick: "Canon in D") ] }
-
5:33 - Layout container
HStack { Label("Whiskers", systemImage: "cat.fill") Spacer() Text("Tightrope walking") }
-
5:41 - Container views
struct ContentView: View { var body: some View { HStack { Image(whiskers.profileImage) VStack(alignment: .leading) { Label("Whiskers", systemImage: "cat.fill") Text("Tightrope walking") } Spacer() } } } let whiskers = Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers") struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String var profileImage: String init(_ name: String, kind: Kind, trick: String, profileImage: String) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage } }
-
6:23 - View modifiers
struct ContentView: View { var body: some View { Image(whiskers.profileImage) .clipShape(.circle) .shadow(radius: 3) .overlay { Circle().stroke(.green, lineWidth: 2) } } } let whiskers = Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers") struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String var profileImage: String init(_ name: String, kind: Kind, trick: String, profileImage: String) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage } }
-
7:05 - Custom views: Intro
struct PetRowView: View { var body: some View { // ... } }
-
7:14 - Custom views
struct PetRowView: View { var body: some View { Image(whiskers.profileImage) .clipShape(.circle) .shadow(radius: 3) .overlay { Circle().stroke(.green, lineWidth: 2) } } } let whiskers = Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers") struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String var profileImage: String init(_ name: String, kind: Kind, trick: String, profileImage: String) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage } }
-
7:20 - Custom views: iteration
struct PetRowView: View { var body: some View { HStack { Image(whiskers.profileImage) .clipShape(.circle) .shadow(radius: 3) .overlay { Circle() .stroke(.green, lineWidth: 2) } Text("Whiskers") Spacer() } } } let whiskers = Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers") struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String var profileImage: String init(_ name: String, kind: Kind, trick: String, profileImage: String) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage } }
-
7:24 - Custom views: view properties
struct PetRowView: View { var body: some View { HStack { profileImage Text("Whiskers") Spacer() } } private var profileImage: some View { Image(whiskers.profileImage) .clipShape(.circle) .shadow(radius: 3) .overlay { Circle().stroke(.green, lineWidth: 2) } } } let whiskers = Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers") struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String var profileImage: String init(_ name: String, kind: Kind, trick: String, profileImage: String) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage } }
-
7:34 - Custom views: complete row view
struct PetRowView: View { var body: some View { HStack { profileImage VStack(alignment: .leading) { Text("Whiskers") Text("Tightrope walking") .font(.subheadline) .foregroundStyle(.secondary) } Spacer() } } private var profileImage: some View { Image(whiskers.profileImage) .clipShape(.circle) .shadow(radius: 3) .overlay { Circle().stroke(.green, lineWidth: 2) } } } let whiskers = Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers") struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String var profileImage: String init(_ name: String, kind: Kind, trick: String, profileImage: String) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage } }
-
7:41 - Custom views: input properties
struct PetRowView: View { var pet: Pet var body: some View { HStack { profileImage VStack(alignment: .leading) { Text(pet.name) Text(pet.trick) .font(.subheadline) .foregroundStyle(.secondary) } Spacer() } } private var profileImage: some View { Image(pet.profileImage) .clipShape(.circle) .shadow(radius: 3) .overlay { Circle().stroke(pet.favoriteColor, lineWidth: 2) } } } struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String var profileImage: String var favoriteColor: Color init(_ name: String, kind: Kind, trick: String, profileImage: String, favoriteColor: Color) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage self.favoriteColor = favoriteColor } }
-
7:53 - Custom views: reuse
PetRowView(pet: model.pet(named: "Whiskers")) PetRowView(pet: model.pet(named: "Roofus")) PetRowView(pet: model.pet(named: "Bubbles"))
-
7:59 - List composition
struct ContentView: View { var model: PetStore var body: some View { List(model.allPets) { pet in PetRowView(pet: pet) } } } @Observable class PetStore { var allPets: [Pet] = [ Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers", favoriteColor: .green), Pet("Roofus", kind: .dog, trick: "Home runs", profileImage: "Roofus", favoriteColor: .blue), Pet("Bubbles", kind: .fish, trick: "100m freestyle", profileImage: "Bubbles", favoriteColor: .orange), Pet("Mango", kind: .bird, trick: "Basketball dunk", profileImage: "Mango", favoriteColor: .green), Pet("Ziggy", kind: .lizard, trick: "Parkour", profileImage: "Ziggy", favoriteColor: .purple), Pet("Sheldon", kind: .turtle, trick: "Kickflip", profileImage: "Sheldon", favoriteColor: .brown), Pet("Chirpy", kind: .bug, trick: "Canon in D", profileImage: "Chirpy", favoriteColor: .orange) ] }
-
8:14 - List composition: ForEach
struct ContentView: View { var model: PetStore var body: some View { List { ForEach(model.allPets) { pet in PetRowView(pet: pet) } } } } @Observable class PetStore { var allPets: [Pet] = [ Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers", favoriteColor: .green), Pet("Roofus", kind: .dog, trick: "Home runs", profileImage: "Roofus", favoriteColor: .blue), Pet("Bubbles", kind: .fish, trick: "100m freestyle", profileImage: "Bubbles", favoriteColor: .orange), Pet("Mango", kind: .bird, trick: "Basketball dunk", profileImage: "Mango", favoriteColor: .green), Pet("Ziggy", kind: .lizard, trick: "Parkour", profileImage: "Ziggy", favoriteColor: .purple), Pet("Sheldon", kind: .turtle, trick: "Kickflip", profileImage: "Sheldon", favoriteColor: .brown), Pet("Chirpy", kind: .bug, trick: "Canon in D", profileImage: "Chirpy", favoriteColor: .orange) ] }
-
8:27 - List composition: sections
struct ContentView: View { var model: PetStore var body: some View { List { Section("My Pets") { ForEach(model.myPets) { pet in PetRowView(pet: pet) } } Section("Other Pets") { ForEach(model.otherPets) { pet in PetRowView(pet: pet) } } } } } @Observable class PetStore { var myPets: [Pet] = [ Pet("Roofus", kind: .dog, trick: "Home runs", profileImage: "Roofus", favoriteColor: .blue), Pet("Sheldon", kind: .turtle, trick: "Kickflip", profileImage: "Sheldon", favoriteColor: .brown), ] var otherPets: [Pet] = [ Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers", favoriteColor: .green), Pet("Bubbles", kind: .fish, trick: "100m freestyle", profileImage: "Bubbles", favoriteColor: .orange), Pet("Mango", kind: .bird, trick: "Basketball dunk", profileImage: "Mango", favoriteColor: .green), Pet("Ziggy", kind: .lizard, trick: "Parkour", profileImage: "Ziggy", favoriteColor: .purple), Pet("Chirpy", kind: .bug, trick: "Canon in D", profileImage: "Chirpy", favoriteColor: .orange) ] }
-
8:36 - List composition: section actions
PetRowView(pet: pet) .swipeActions(edge: .leading) { Button("Award", systemImage: "trophy") { // Give pet award } .tint(.orange) ShareLink(item: pet, preview: SharePreview("Pet", image: Image(pet.name))) }
-
9:31 - View updates
struct ContentView: View { var model: PetStore var body: some View { List { Section("My Pets") { ForEach(model.myPets) { pet in row(pet: pet) } } Section("Other Pets") { ForEach(model.otherPets) { pet in row(pet: pet) } } } } private func row(pet: Pet) -> some View { PetRowView(pet: pet) .swipeActions(edge: .leading) { Button("Award", systemImage: "trophy") { pet.giveAward() } .tint(.orange) ShareLink(item: pet, preview: SharePreview("Pet", image: Image(pet.name))) } } } struct PetRowView: View { var pet: Pet var body: some View { HStack { profileImage VStack(alignment: .leading) { HStack(alignment: .firstTextBaseline) { Text(pet.name) if pet.hasAward { Image(systemName: "trophy.fill") .foregroundStyle(.orange) } } Text(pet.trick) .font(.subheadline) .foregroundStyle(.secondary) } Spacer() } } private var profileImage: some View { Image(pet.profileImage) .clipShape(.circle) .shadow(radius: 3) .overlay { Circle().stroke(pet.favoriteColor, lineWidth: 2) } } } @Observable class PetStore { var myPets: [Pet] = [ Pet("Roofus", kind: .dog, trick: "Home runs", profileImage: "Roofus", favoriteColor: .blue), Pet("Sheldon", kind: .turtle, trick: "Kickflip", profileImage: "Sheldon", favoriteColor: .brown), ] var otherPets: [Pet] = [ Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers", favoriteColor: .green), Pet("Bubbles", kind: .fish, trick: "100m freestyle", profileImage: "Bubbles", favoriteColor: .orange), Pet("Mango", kind: .bird, trick: "Basketball dunk", profileImage: "Mango", favoriteColor: .green), Pet("Ziggy", kind: .lizard, trick: "Parkour", profileImage: "Ziggy", favoriteColor: .purple), Pet("Chirpy", kind: .bug, trick: "Canon in D", profileImage: "Chirpy", favoriteColor: .orange) ] } @Observable class Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } var name: String var kind: Kind var trick: String var profileImage: String var favoriteColor: Color var hasAward: Bool = false init(_ name: String, kind: Kind, trick: String, profileImage: String, favoriteColor: Color) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage self.favoriteColor = favoriteColor } func giveAward() { hasAward = true } } extension Pet: Transferable { static var transferRepresentation: some TransferRepresentation { ProxyRepresentation { $0.name } } }
-
10:57 - State changes
struct RatingView: View { @State var rating: Int = 5 var body: some View { HStack { Button("Decrease", systemImage: "minus.circle") { rating -= 1 } .disabled(rating == 0) .labelStyle(.iconOnly) Text(rating, format: .number.precision(.integerLength(2))) .font(.title.bold()) Button("Increase", systemImage: "plus.circle") { rating += 1 } .disabled(rating == 10) .labelStyle(.iconOnly) } } }
-
11:51 - State changes: animation
struct RatingView: View { @State var rating: Int = 5 var body: some View { HStack { Button("Decrease", systemImage: "minus.circle") { withAnimation { rating -= 1 } } .disabled(rating == 0) .labelStyle(.iconOnly) Text(rating, format: .number.precision(.integerLength(2))) .font(.title.bold()) Button("Increase", systemImage: "plus.circle") { withAnimation { rating += 1 } } .disabled(rating == 10) .labelStyle(.iconOnly) } } }
-
12:05 - State changes: text content transition
struct RatingView: View { @State var rating: Int = 5 var body: some View { HStack { Button("Decrease", systemImage: "minus.circle") { withAnimation { rating -= 1 } } .disabled(rating == 0) .labelStyle(.iconOnly) Text(rating, format: .number.precision(.integerLength(2))) .contentTransition(.numericText(value: Double(rating))) .font(.title.bold()) Button("Increase", systemImage: "plus.circle") { withAnimation { rating += 1 } } .disabled(rating == 10) .labelStyle(.iconOnly) } } }
-
12:22 - State changes: multiple state
struct RatingContainerView: View { @State private var rating: Int = 5 var body: some View { Gauge(value: Double(rating), in: 0...10) { Text("Rating") } RatingView() } } struct RatingView: View { @State var rating: Int = 5 var body: some View { HStack { Button("Decrease", systemImage: "minus.circle") { withAnimation { rating -= 1 } } .disabled(rating == 0) .labelStyle(.iconOnly) Text(rating, format: .number.precision(.integerLength(2))) .contentTransition(.numericText(value: Double(rating))) .font(.title.bold()) Button("Increase", systemImage: "plus.circle") { withAnimation { rating += 1 } } .disabled(rating == 10) .labelStyle(.iconOnly) } } }
-
12:45 - State changes: state and binding
struct RatingContainerView: View { @State private var rating: Int = 5 var body: some View { Gauge(value: Double(rating), in: 0...10) { Text("Rating") } RatingView(rating: $rating) } } struct RatingView: View { @Binding var rating: Int var body: some View { HStack { Button("Decrease", systemImage: "minus.circle") { withAnimation { rating -= 1 } } .disabled(rating == 0) .labelStyle(.iconOnly) Text(rating, format: .number.precision(.integerLength(2))) .contentTransition(.numericText(value: Double(rating))) .font(.title.bold()) Button("Increase", systemImage: "plus.circle") { withAnimation { rating += 1 } } .disabled(rating == 10) .labelStyle(.iconOnly) } } }
-
14:16 - Adaptive buttons
Button("Reward", systemImage: "trophy") { // Give pet award } // .buttonStyle(.borderless) // .buttonStyle(.bordered) // .buttonStyle(.borderedProminent)
-
14:53 - Adaptive toggles
Toggle("Nocturnal Mode", systemImage: "moon", isOn: $pet.isNocturnal) // .toggleStyle(.switch) // .toggleStyle(.checkbox) // .toggleStyle(.button)
-
15:19 - Searchable
struct PetListView: View { @Bindable var viewModel: PetStoreViewModel var body: some View { List { Section("My Pets") { ForEach(viewModel.myPets) { pet in row(pet: pet) } } Section("Other Pets") { ForEach(viewModel.otherPets) { pet in row(pet: pet) } } } .searchable(text: $viewModel.searchText) } private func row(pet: Pet) -> some View { PetRowView(pet: pet) .swipeActions(edge: .leading) { Button("Reward", systemImage: "trophy") { pet.giveAward() } .tint(.orange) ShareLink(item: pet, preview: SharePreview("Pet", image: Image(pet.name))) } } } @Observable class PetStoreViewModel { var petStore: PetStore var searchText: String = "" init(petStore: PetStore) { self.petStore = petStore } var myPets: [Pet] { // For illustration purposes only. The filtered pets should be cached. petStore.myPets.filter { searchText.isEmpty || $0.name.contains(searchText) } } var otherPets: [Pet] { // For illustration purposes only. The filtered pets should be cached. petStore.otherPets.filter { searchText.isEmpty || $0.name.contains(searchText) } } }
-
15:20 - Searchable: customization
struct PetListView: View { @Bindable var viewModel: PetStoreViewModel var body: some View { List { Section("My Pets") { ForEach(viewModel.myPets) { pet in row(pet: pet) } } Section("Other Pets") { ForEach(viewModel.otherPets) { pet in row(pet: pet) } } } .searchable(text: $viewModel.searchText, editableTokens: $viewModel.searchTokens) { $token in Label(token.kind.name, systemImage: token.kind.systemImage) } .searchScopes($viewModel.searchScope) { Text("All Pets").tag(PetStoreViewModel.SearchScope.allPets) Text("My Pets").tag(PetStoreViewModel.SearchScope.myPets) Text("Other Pets").tag(PetStoreViewModel.SearchScope.otherPets) } .searchSuggestions { PetSearchSuggestions(viewModel: viewModel) } } private func row(pet: Pet) -> some View { PetRowView(pet: pet) .swipeActions(edge: .leading) { Button("Reward", systemImage: "trophy") { pet.giveAward() } .tint(.orange) ShareLink(item: pet, preview: SharePreview("Pet", image: Image(pet.name))) } } }
-
16:58 - App definition
@main struct SwiftUIEssentialsApp: App { var body: some Scene { WindowGroup { ContentView() } } }
-
17:15 - App definition: multiple scenes
@main struct SwiftUIEssentialsApp: App { var body: some Scene { WindowGroup { ContentView() } WindowGroup("Training History", id: "history", for: TrainingHistory.ID.self) { $id in TrainingHistoryView(historyID: id) } WindowGroup("Pet Detail", id: "detail", for: Pet.ID.self) { $id in PetDetailView(petID: id) } } }
-
17:23 - Widgets
struct ScoreboardWidget: Widget { var body: some WidgetConfiguration { // ... } } struct ScoreboardWidgetView: View { var petTrick: PetTrick var body: some View { ScoreCard(rating: petTrick.rating) .overlay(alignment: .bottom) { Text(petTrick.pet.name) .padding() } .widgetURL(petTrick.pet.url) } }
-
19:37 - Digital Crown rotation
ScoreCardStack(rating: $rating) .focusable() #if os(watchOS) .digitalCrownRotation($rating, from: 0, through: 10) #endif
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。