-
SwiftUI 基础知识:使用 SwiftUI 构建卓越的 App
在这场从库比提诺 Apple Developer Center 直播的全天线上特别活动中,了解如何使用 SwiftUI 构建卓越的 App。无论你是刚刚入门,还是已有 SwiftUI 开发经验,这一系列基础知识讲座都将帮助你加深对核心概念的理解,并编写出强大且高效的代码。向 AllTrails 的首席技术官 James Graham 学习,了解他的公司如何充分利用 SwiftUI 的功能。此外,通过参与 SwiftUI 工程团队成员问答环节,深入了解相关内容。
资源
-
搜索此视频…
喂 太棒了!我知道,很有趣。
大家好,欢迎来到位于 库比蒂诺的 Apple 开发者中心。 我叫莉娅 · 沃梅尔斯多夫, 是一名技术布道师。
今天,我们很高兴向大家介绍 SwiftUI 的基础知识, 内容相当丰富。 但首先,我想向大家详细 介绍一下我们的活动场地。
开发者中心是 Apple Park 的部分, 是举办今天活动的绝佳场所。 这里设有多个区域,方便大家交流与协作。
这个房间名为“Big Sur”, 是一个布置精美、环境优美的空间。 它专为支持各类活动而设计, 包括现场演讲、录音棚录制以及直播。
此外,这里还有实验室、 简报室和会议室,让我们能够 举办像这样以及更多类型的活动。
这是我们遍布全球的四家开发者中心之一,我们 在这里为设计师和开发者举办讲座、 实验室和研讨会。 在座的各位中,有没有人去过开发者中心? 太好了。欢迎回来。无论你是新朋友还是老朋友, 我们都很高兴今天能欢迎你的到来。
此外,此刻还有许多朋友通过线上方式参与其中。 大家好,感谢收看。我们的开发者 关系团队非常乐于与开发者们互动, 共同打造适用于 Apple 平台的最佳应用。
自 WWDC 以来,我们已举办了 300 多场活动,包括实践实验室、 工作坊和演讲。
本周晚些时候,我的团队将 在库比蒂诺主持一场关于新设计的工作坊。 开发者们将在更新代码和设计时, 亲身体验 Liquid Glass 效果。
如需了解您附近的即将举办的活动, 请访问 developer. apple.com/events。 在 开始今天的议程之前, 我想向现场的各位分享一些小贴士,
帮助大家在全天活动中保持顺畅连接。 请使用 Apple Wi-Fi 网络; 如果需要为设备充电, 每个座位都配有电源插座。 对于今天在线参与的各位, 电源插座位于扶手前方。 无论线上还是线下,这些演讲都旨 在为您带来一场专属的特别体验。 因此,请在演示期间不要录制视频或进行直播。 不过,拍照是完全允许的,请尽情拍摄。
活动结束后,我们将发送后续信息及相关资源, 确保您不会错过任何内容。
在说明了这一小点要求之后,我很兴奋能与大家 分享更多关于今天活动的内容。
SwiftUI 是 Apple 的声明式用户界面框架。
与任何工具一样, SwiftUI
也有其基础概念。 花时间学习这些概念,将使您能够熟练运用它, 并打造出令人惊叹的作品。
借助 SwiftUI, 最终成果不仅仅是一个应用程序, 而是一款能在所有 Apple 硬件上蓬勃发展的应用程序。 它直观且熟悉,即使是 初次使用的人也能轻松上手。
SwiftUI 应用能够随着操作 系统的最新更新保持现代感。 就像 Liquid Glass 一样, SwiftUI 在 Apple 内部被 广泛应用,既作为新应用的基础, 也用于演进那些最初基于 UIKit 开发的应用。
它的设计充分考虑了渐进式采用, 因此你可以逐步将其融入你的代码库中。
如今,比以往任何时候都更需要花时间学习 SwiftUI 的基础知识显得尤为重要。
编写代码的方式多种多样,无论是逐行编写, 还是使用大型语言模型(LLMs)和智能代理。 掌握基础知识将帮助你更好地理解代码, 并对自己的代码更有信心。
我们的团队一直在开发一款名 为“Wishlist”的新应用, 它 100% 由 SwiftUI 构建而成。
“Wishlist”是今天所有演示的基石, 将展示 SwiftUI 的基础知识如何融合 在一起,最终打造出一个真正的应用。
我和我的队友们都热爱旅行, 而规划下一次假期对我来说充满乐趣。
我喜欢进行详尽的规划, 以便充分利用每次旅行—— 不仅包括要去的地方,还包括在那里要做的事情。
“Wishlist”是一款旅行愿望清单应用。
我们开发这款应用是为了记录想去的地方、 想做的活动, 并在追逐梦想、尝试新事物的过程中, 通过自我督促来反思我们的进展。
应用中的数据主要分为两类:行程和活动。
“行程”是指我想去的地方,比如夏威夷的科纳;
“活动”则是我在旅途中想做的事情, 比如浮潜和观鲸。
应用中有三项主要功能。 点击工具栏中的“+”按钮, 我可以规划新的旅行, 添加标题、照片和出行季节。
在旅行期间,我可以从清单中勾选已完成的活动, 比如与蝠鲼一起浮潜。
我还可以回顾自己的进展。 例如,今年秋天我去了蒙大拿州旅行, 并获得了“秋季探险家”徽章, 以此记录这些经历。 “愿望清单”采用了标签页视图。
我们选择标签页视图是为了保持导航的一致性。 我的同事 Majo 将在她的设计演示 中解释其中的一些设计决策。
“愿望清单”应用中的“愿望 清单”标签页包含三个标签。 我可以向下滚动浏览所有已计划的旅行, 或添加新的旅行计划。
当我点击“科纳深潜”这样的卡片时, 系统会跳转到“行程详情”页面, 我可以在那里勾选已完成的项目。
“目标”标签页是查看我 距离获得奖章还有多远的地方。 顶部一排显示了我最近获得的奖项, 例如“秋季探险家”。
向下滚动时,我可以查看 自己距离下一个里程碑的进度。 我已经完成了七项活动, 距离获得“十项活动”奖项仅差三项。
第三个标签页是
“搜索”标签页。 我使用搜索功能快速找到所需内容。 当我已经添加了大量行程后, 通过搜索功能快速定位到我 想要的具体行程,真的非常有用。
我的同事们会向大家详细 介绍我们如何构建这款应用, 但更重要的是,他们会阐述 支撑每个决策的基础理念。
这些基础理念是今天大家最需要带走的收获。 请将这些理论应用到你们自己的代码中, 这样你们就能充满信心地对代码进行推理。
制作“愿望清单”的过程非常有趣。 从构思到设计, 再到实际构建这款应用,每一 步都为今天后续的演讲奠定了基础。 在接下来的几周内, 我们将把“愿望清单”的示例代码发布 在我们的网站上供大家下载。 对此我感到非常兴奋。 这是跟随讲解并巩固所学知识的绝佳方式。 如果您对示例或 SwiftUI 本身有任何疑问, 可以使用 Slido 向 Apple 工程师提问。
我们的团队热切期待并随时准备为您提供支持,
您可以就示例或任何 SwiftUI 相关问题进行提问。
您还可以为其他开发者的提问点赞, 并阅读相关回答。 这是一个绝佳的学习资源, 因此我建议大家务必去了解一下。
好的。现在是时候深入了解今天的议程了。
今天的演讲将涵盖 SwiftUI 的基础主题。
这一点非常重要。 无论您是初学者,还是已有 经验并希望加深对该 框架的理解,这些基础知识都大有裨益。
即使您已经熟悉 SwiftUI, 对基础知识的深入理解也 将帮助您选择更优的实现策略, 并解决应用中的问题。
我将首先对 SwiftUI 进行概述, 以此拉开本次活动的序幕。
随后, Majo 将分享 SwiftUI 如何助你利用系统进行设计。
之后我们将短暂休息, 并于太平洋时间 11:15 重新开始。
随后, Cat 将带大家深入了解 SwiftUI 的布局系统。
接着, Curt 将带来“动态 SwiftUI”主题演讲, 讲解动画和视觉特效的相关技术。
之后我们将休息午餐,稍作休息并补充能量。 太平洋时间 13:00, 我们将在这里重新聚首, 届时 Cole 将演示如何基于意图进行建模。 在 SwiftUI 中基于 意图对数据进行建模。
随后我们将迎来一位特别嘉宾。 詹姆斯 · 格雷厄姆, AllTrails 的首席技术官, 将分享他们从复杂且 成熟的 UIKit 应用逐步迁移至 SwiftUI 过程中获得的关键经验。
我们将再休息一次,伸展一下身体并喝杯咖啡, 随后由 SwiftUI 工程领域的领军 人物们进行小组讨论,为今天画上句号。 他们将解答今天最热门的一些问题, 并向在场的库比蒂诺同仁 分享他们对该框架的展望。 之后,我们将举办一场交流会, 提供一些简便的小点心, 大家也有机会与苹果的工程师和设计师交流互动。
好的,现在我将以关于《 SwiftUI 基础》的演讲拉开本次活动的序幕。
我是莉亚,是一名技术布道师。 在加入技术推广团队之前, 我曾是 Apple 公司工程师。
我参与开发过一些我最喜欢的应用, 比如“健身”应用、 “锻炼”应用和“健康”应用, 甚至还在 2021 年的主题演讲中短暂出镜。 这很酷,对吧? 拍摄过程超级有趣。 我还得以在 Apple Park 里跑来跑去。 我于 2019 年加入苹果, 那一年正是 SwiftUI 发布之年。
这些年来,我对这个框架有了很多了解, 不仅学会了如何使用它, 更重要的是,学会了如何善用它。
刚开始使用 SwiftUI 时, 我特别喜欢它能让我用 几行代码快速制作原型并 构建应用中真正可用的功能, 我能做出真实的东西。 这感觉太棒了。
这是许多软件工程师都会经历的过程, 无论使用的是什么工具或框架。 看到成果呈现在屏幕上确实令人兴奋, 但有时它并不总能按预期运行。 因此,我需要退一步,从基础开始。
我需要理解 SwiftUI 的工作原理, 这样才能开发出更高质量的软件。
今天,我将分享一些关于 SwiftUI 工作原理的核心概念。
这些概念将帮助你编写更优质的代码, 同时也为今天后续的讲解奠定基础。
首先,我将深入探讨 SwiftUI 视图中一个被频繁使用的术语。
接着,我将介绍 SwiftUI 内置的视图,
以及创建新自定义视图背后的逻辑。
最后,我将讲解依赖关系, 以及如何有意识地组织代码结构。
在每个部分中,我都会分享该部分最重要的概念, 以及它们与编写优质 SwiftUI 代码这一大局之间的关联。
好的。在 SwiftUI 中, “视图”是一个多义词。 根据上下文的不同,它可能有三种不同的含义。 这可能会让人有些困惑。
在许多框架中,“视图” 指的是用户界面和屏幕上的像素。
而在 SwiftUI 中, “视图”也是一种协议。 它是你希望在屏幕上呈现内容的描述。
由于每个视图都是一个结构体, 因此每个视图实例都有一个特定的值。
这三个概念是相互关联的。 屏幕上的像素是视图描述以及该 特定视图结构体实际值的呈现结果。
视图的描述及 其驱动的值共同构成了屏幕上最终显示的像素。
“视图”的这三种含义 会在应用的不同领域中出现。
描述即代码本身。
值则是内存中的实例。 因此, SwiftUI
可以渲染像素,而屏幕上的像素需要显示最新、 最准确的信息。
在接下来的演讲中,我将逐一探讨“视图”的这 三种不同含义。
上下文非常重要, 它将帮助你理清自己在 SwiftUI 代码中遇到的问题。
我将从“视图作为描述”开始讲起。
在 SwiftUI 中, 视图是带有明确构建方式的描述。
假设我想展示一张酷炫的水下场景图片, 画面中有人在浮潜。
为了描述这一点, 我会写下“水下浮潜的图片”这句话。
在 SwiftUI 中, 我通过视图协议来构建这个描述。
`Image` 是 SwiftUI 视图, 而 `Deep Sea` 是图片的名称,
就像我用英语描述的那样。 视图。描述我在 SwiftUI 中想要的内容。
SwiftUI 会根据该视图决定 在屏幕上渲染什么,就像这样。 很棒的照片。
所有 SwiftUI 视图 都是用代码编写的描述。
无论它们是内置视图,还是你 自己编写的自定义视图,情况都是如此。
`Image` 就是 SwiftUI 内置视图的一个例子。
视图是每个应用的构建模块, 而内置视图是入门的绝佳途径。 SwiftUI 中有许多内置视图, 每个视图的名称都描述了它所呈现的内容。
例如,就像 `image` 视图用于显示图片一样,
`color` 视图会将整个 画布填充为特定颜色, 比如紫色。
内置视图之所以强大,是因为 SwiftUI 将其与硬件进行了深度集成。
这里,这种紫色实际上是一种基于上下文的紫色。
实际的颜色值会根据设备的上下文进行调整, 例如手机处于浅色模式还是深色模式, 或者屏幕上是否有强烈的阳光照射。
“文本”视图用于呈现字符串, 例如“Kona Deep Dive”。
与“颜色”视图类似,“文本” 视图也会根据其所在的环境进行优化。
SwiftUI 会使用适合当前 平台的字体来绘制字符串。 在 iMac 等大屏幕设备上, “文本”视图的物理尺寸 会比 Apple Watch 等小屏幕设备上的更大。
“文本”视图还支持动态类型(Dynamic Type ),这是一项辅助功能。
“动态类型”允许用户 调整设备上可见文本的大小, 以便舒适地阅读。
在此示例中,右侧手机的系统字体大小较大。
我 SwiftUI 代码中的文本 视图会自动调整大小, 以确保内容依然清晰可读, 而这并不需要我额外编写任何代码。
对于每个视图、图像、颜色和文本, 其名称都是我想要效果的描述,
结合了“深海”、“紫色”和“科纳深潜”等值。 这些最终会以像素的形式呈现在屏幕上。
内置视图是一个绝佳的起点, 因为它们不仅开箱即用,还支持自定义。
自定义视图既能展现应用的独特个性, 又能充分利用 SwiftUI 内置视图的优势。
在 SwiftUI 中有多种 方法可以自定义视图。
我之前创建的 TextView 看起来还不错, 但我可以使其更具辨识度。
视图修饰符是用于自定义单个视图的工具。 接下来我将深入探讨一些代码。
自定义文本视图的方法有很多。 例如,更改字体的大小或颜色。 这是我们在“愿望清单”应用中经常做的事情。 目前,我先从一个简单的例子开始。 我想让我的文本视图 在明亮的颜色背景下显得格外醒目。
背景修饰符用于设置视图的背景。 你可以将任何视图作为背景。 我选择了橙色。
背景就是视图修饰器的一个示例。
视图修饰器是一种针对特定 视图(如文本视图)调用的方法, 它会返回一个包含原始视图的新视图。
原始文本视图被视图修饰器带来的变化所包裹, 从而生成一个新视图。
我喜欢将视图修饰器视为包裹视图的外壳。
综合来看,这两行代码生成了带有橙色背景的原始 字符串“Kona Deep Dive”。 这是对原始文本视图的轻微修改版本。
你可以应用多个视图修饰 符来创建复合的自定义效果。
填充(Padding)是另一种视图修饰符。
它会在应用对象的边缘添加点。 在此示例中,填充在文本视图的四边都添加了点, 随后橙色背景将其填充,就 像 backgroundPadding 包裹了它前面的所有内容一样。 因此,这三行代码在视图层次结构 中代表了一个大型视图。
关于视图修饰符还有一点需要注意。 顺序很重要。 每个视图修饰符仅影响其上方的代码行。
如果我调换顺序,让 background 排在最前面。 橙色只会覆盖原始文本视图。
灰色虚线框所示的内边距则应用于其余部分。
请有意识地安排视图修饰符的顺序。 如果代码行为与预期不符, 请检查确认视图的包裹顺序是否合理。
虽然视图修饰符用于自定义单个视图, 但组合是一种将多个视图结合起来的技术。
你可以通过组合现有 视图来构建自己的自定义视图。
假设我想在“愿望清单”应用的“ 科纳深潜”搜索标签页中构建这样一行。
我可以使用之前创建的一些内置视图。
首先,我将定义一个名 为 SearchRow 的视图。
它是一个结构体,并且符合视图协议。
然后,我将添加视图的主体内容。 这是所有视图都必须具备的部分。
我将添加一个深海主题的图像视图,以及一个显示
“Kona Deep Dive ”字样的 TextView。
默认情况下, SwiftUI 会将它们垂直堆叠。
我希望它们并排显示, 因此将添加一个水平堆栈( HStack),例如图像和文本。 HStack
是一种视图,但它不像 图像那样描述要渲染的内容, 而是描述如何渲染——即 SwiftUI 如何将所有子视图(如图片 和文本)以水平线的形式排列。
今天稍后, Cat 将深入讲解 SwiftUI 中布局的工具和技巧。 即使在制作更复杂的视图时, 我也想强调一点:核心概念是相同的。 我的 SearchRow
描述了我想要的效果, 而 SwiftUI 会根据该 描述在屏幕上渲染像素。
组合使代码更易于阅读,
一旦构建好 SearchRow, 只需一行 代码即可在应用的其他部分使用它。
在我的搜索视图中,无需使用三个堆栈、 三张图片和三个文本视图。 我只需使用三个组合的 SearchRow 视图,
而且后续如果想修改 SearchRow 中的任何内容(例如将背景设为紫色), 我可以在该视图内部直接进行调整。
这不仅减少了需要编写的代码量, 还使代码更易于阅读和理解。
因为视图本质上是结构体。 它们非常轻量,因此像这样将一个视图 拆分为更小的视图并不会影响性能。
与内置视图一样, 自定义视图也是我们对 SwiftUI 应如何 在屏幕上绘制内容的描述。
像 SearchItemView 这样的名称固然重要,但更重要的是 body 内部的视图及其排列顺序。 SwiftUI 会根据 代码中的描述在屏幕上绘制像素。
现在我想更进一步。 我的 SearchRow 已经 非常接近预期设计,但还差一点。
目前我有三个 KonaDeep Dive 的实例,不知道你是否也是这样, 但我喜欢每年更换度假目的地。 应用程序是动态的,它所呈现的数据会发生变化。
我需要一种方法来切换这些内容, 这样才能展示我愿望清单上的其他旅行计划。
这就引出了我的最后一个话题。 依赖关系。
SwiftUI 视图由数据驱动。 无论这些数据是保持不变的( 比如我的深海图片和字符串“ Kona Deep Dive”), 还是会发生变化。
我今天展示的所有视图都依赖于数据。
屏幕上的像素部分源于“ image”这样的描述, 部分源于“Deep Sea”这样的数据。
我之前提到过,所有视图都是结构体 (struct), 这意味着所有视图都是值类型, 每个视图实例都有一个值。
我喜欢这样将视图实例可视化: 顶部是视图名称(如 image), 底部是数据(如 Deep Sea)。
SwiftUI 会取一个特定的视图 实例(包括其中的数据), 以此在屏幕上渲染像素。
一旦像素渲染完成, SwiftUI 就会丢弃该实例。 它不再需要该实例了。
这就是“视图”的第三层含义。 视图作为值。
视图的实例。这些具体的值驱动着屏幕上的像素。
每个视图实例都只是短暂存在的。 它们寿命很短。它们在需要时被创建, 并驻留在 SwiftUI 的内存中。 一旦像素渲染完成, 视图的任务就完成了,实例也会被丢弃。
请注意,丢弃它们并非坏事,也并非高风险操作。 视图轻量且易于创建。
我喜欢将视图视为模板, 能够批量生成相同的内容, 从而在屏幕上生成像素。 很多时候,这些模板代表着灵活且动态的数据。
例如,在我的应用中,我一直专注于深海主题, 但同时也需要一张京都的图片。
这也是一个视图实例, 但它的值略有不同。 图片名称“京都”与“深海”不同。
SwiftUI 会根据图片名称(即 不同值)处理这些 ImageView, 并生成不同的图片。
现在,我将利用这种灵活性, 重新审视我的 SearchRow 代码。
现在,不再硬编码图片名称和文本, 我的 SearchRow 现在会接受 图片名称和行程标题作为参数。
这个版本的 SearchRow 更加灵活。 我仍然可以用它来制作“科纳深潜”行, 但也可以用于其他行程。
这让我更接近实际的设计。
当我在搜索视图中使用 新的 SearchRow 时,
我会提供所有不同的图片和行程名称, 例如“Mammoth Blush” 和“Kyoto Mystique”。
这样要好得多。
数据驱动着屏幕上的像素显示, 同时我仍能利用组合模式中简洁的代码。
请注意,此搜索视图的实现方式仍是 “愿望清单”中最终版本的简化版。
实际的搜索功能会动态查询数据, 然后根据搜索字段中输入的图片 和字符串来填充搜索行。
我想再强调一下关于这些搜索行的一点。
就像之前的图像视图一样, 每个 SearchRow 实例都会根据 行程名称和照片名称拥有不同的值。
SwiftUI 可以轻松地比较这些值, 并发现它们并不相等。
在 SwiftUI 将像素绘制后, 会释放这些视图的实例。 由于像素已经渲染完成, 它不再需要这些视图实例。
现在我想进一步探讨 SwiftUI 如何处理依赖关系。
对于我的 SearchRow, 描述就是我的视图代码。
视图的不同值会在屏幕上呈现出不同的像素。
SwiftUI 通过依赖关系来追踪重要值, 这里所说的“重要值”是指那些会根据 数据使像素显示正确或错误的值。
其工作原理如下。
SwiftUI 首次运行 body 时, 会通过一个图来记录它读取的所有值。
该图的第一部分是 SearchRow 的节点。
在 `body` 中, SwiftUI 读取了图像视图中“照片名称”的值。
因此,它为“照片名称”添加了一个节点, 并添加了一条指向 SearchRow 的箭头。
SwiftUI 还读取了“行程名称”的值, 因此为“行程名称”添加了一个节点和一条箭头。 SwiftUI 会追踪这些值, 因为要使像素显示正确,它们必须对应正确的“
照片名称”和“行程名称”。 如果其中任何一个发生变化。 例如,如果我提供一个新的照片 名称(我用这个红点表示), 那么 SearchRow 的像素 就会过时, SwiftUI 就需要绘制新的像素。
依赖关系就像视图的输入。
依赖追踪是 SwiftUI 能够高效 进行更新的原因之一。
在拥有大量视图和海量数据的应用程序中, 如果每次有某项内容发生变化, SwiftUI 就 必须重新绘制所有内容,这将极其低效。
应用中的数据变化频繁, 这会导致大量不必要的更新。
相反, SwiftUI 会追踪依赖关系。
我在此用箭头来表示, 它们指示哪些视图依赖于特定的数据。
借助这个系统,当某处发生变化时, SwiftUI 只会重新 计算并重绘下游的内容。
在应用中,事物变化迅速且频繁, 依赖追踪机制确保了 SwiftUI 的高效运行。
这虽然机制复杂,但有个好消息。 你无需自己构建这个图。
SwiftUI 会通过其 依赖图自动为你构建它。
它会记录每个视图
在每次执行 `body`
时读取的属性。 该系统确保依赖图始终保持最新状态。
尽管您无需构建或维护这个依赖图, 但在编写代码时仍需注意以下几点: 构建视图时应保持其轻量级,考虑您已告知 SwiftUI 在 `body` 中需要哪些信息, 以及这些视图的更改是否会导致整个视图失效。
最好保持视图轻量且依赖关系最少, 这样 SwiftUI 只会在必要时重绘它们。
这并不意味着你不能使用复杂的视图。
这意味着你应该将这些复杂的视图 分解成更小、更简单的部分, 就像我之前对搜索视图中的行所做的那样。
使用合适的工具来表示你的数据, 并确保仅在需要更改屏幕像素时才传递更改。 视图依赖数据的方式有几种。 一种方法是像我之前那样, 通过提供图片和行程的具体名称, 将数据直接传递给视图。
创建数据另一种方法是 使用 `at state`。
我的同事 Cole 将在他的演讲 中更深入地探讨 `at state` 以及其他数据流选项, 但我希望先介绍一些基础知识。
用 `at state` 包装的属性。 这会告诉 SwiftUI 在视图的整个生命周期内保存该信息。
即视图位于视图层次结构中的整个时间段。 我将通过一个简单示例进行演示。
在“愿望清单”的搜索标签页中, 当我开始在搜索栏中输入内容时, 搜索结果会实时筛选。
当我输入字母 J 时, 最近的项目会被替换为包含搜索 字符串的行程和活动, 例如“徒步”、“约书亚树” 和“悬崖跳水”。
当我再输入另一个字母 O 时, 结果会继续筛选。
在我添加到应用中的所有行程和活动中, “约书亚树”是唯一包含字符串j o的项目。
这就是应用的状态。我输入到搜索 字段中的字符串驱动着搜索 结果和屏幕上的像素显示。
我将构建一个简化的示例, 来演示“状态”是如何工作的。
这是搜索视图的简化版本, 我将其命名为“简单搜索”。
它具备真实搜索视图的核心功能, 但在设计上进行了一些简化。
主体中只有两个视图。 我使用了一个文本字段和一个文本视图。
该文本视图只是一个显示“结果在此”的占位符。 这是我在制作原型时经常做的一件事。 我会放入占位符,最终会将其 替换为一个显示搜索结果的自定义视图。
TextField 是另 一个 SwiftUI 内置视图。 它用于收集输入内容。 当用户点击它时, 键盘会弹出,并显示我输入的字符串。
我在这里提供了一个起始提示, 然后告诉 SwiftUI 将输入的字符串存储 在名为 Search Value 的变量中。
美元符号是绑定(Binding)的语法, 这是一种数据流工具, Cole 稍后会详细介绍。
我使用 At 状态属性包装 器对 searchValue 属性进行了装饰。
这会告诉 SwiftUI 在更新过程中 保留 searchValue 的值。
初始值为空字符串。
当我点击文本字段时, 光标会闪烁,表示可以输入内容。
当我输入字母 J 时, `searchValue` 的值会从空字 符串变为 j(SwiftUI)。 保留该值后,即可据此更新 SimpleSearch 的像素显示。
当我添加字母 O 继续筛选至“Joshua Tree”时, SwiftUI 会保留搜索字 符串“j o”的值, 从而在更新过程中保持连续的进度。
`At state` 是建模 那些应在视图整个生命周期 内保持不变的信息的最简单方法。
只要我处于搜索标签页中,搜索值就会一直保留, 因此我可以继续向其中添加内容。
如果我离开该标签页,然后稍后返回, 该值将重置为空字符串。
这只是在“愿望清单”中构 建真实搜索视图的第一步。 接下来,我需要用与搜索值字符串 匹配的旅行和活动替换占位符文本视图。
目前,请先专注这些状态方面的内容。
“At state”是 SwiftUI 中的一个属性包装器。
当一个属性被标记 为.At state, SwiftUI 便 会将其视为应用的“真实数据源”, 并在更新过程中持续保留该值。
状态会在视图的整个生命周期内保存在内存中。
这意味着应用可以显示动态且复合的数据, 例如文本字段中输入的字符串。
在 SwiftUI 中有多种 方式来构建依赖关系, Cole 稍后会详细介绍每种方式。 目前,我想强调一点:请花 时间了解这些工具的工作原理。 这样,你就能确保自己构建的依赖 关系能够与 SwiftUI 协同工作。
正如我今天所介绍的, 视图是 SwiftUI 的构建模块。 通过更深入地了解它们在不同场景中的应用, 你不仅能理解该框架, 还能提升代码质量。
关于今天剩余的演讲内容。 在探索 Wishlist 的不同模块时, 我建议大家重点关注这些 解决方案背后的理论原理。
这样,你就能将这些知识应用到自己的应用中。
关于我的演讲内容,请牢记以下
几点:利用 SwiftUI 的内置 视图来创建自己的自定义视图。 探索不同的修饰符,以展现你应用的独特之处。
在构建这些视图或重构现有视图时, 请记住要保持视图的轻量级。 考虑每个视图真正应该依赖哪些数据, 并将复杂的视图分解为更简单、更小的部分。
最后,请花时间深入学习 SwiftUI。 牢记这些基础原则, 你将更有能力应对后续出现的各种挑战。 非常感谢大家参与本次活动。 现在,我很荣幸将话筒交给我的同事 Majo。 她将向大家详细介绍她如何 利用该系统设计“愿望清单”。 请和我一起欢迎 Majo 上台。
大家好。感谢各位的到来。 哦,非常抱歉。控制台出了点问题。 能否把演讲稿放大一点?我看不清。
感谢大家与整个团队一同前来。 我们专门为各位和本次活动设计了“愿望清单”, 非常激动能与大家分享这一 设计过程是如何付诸实践的。 希望你们能将我这些年来积累的一些经验 和设计流程,融入到你们自己的工作中。 那么, 让我们开始吧。 我将从应用导航入手,第一步是为内容 和功能创建具体的板块。 这大概就是我喜欢开始的方式。 我们需要在明确究竟要构建 什么的基础上开始规划。 导航这个话题虽然听起来有些枯燥, 但我个人非常喜欢。 所以我们就从这里开始吧。 接下来,我将探讨 布局,并讨论如何在屏幕上以最佳方式呈现内容。
最后,我将深入讲解视觉设计。 正是这些组件和视觉 元素的有机结合,才能为你的应用 带来清晰度与独特个性。
好的,正如我提到的, 我是从零开始设计这 款应用的,而我做的第一 件事就是思考应用的导航——
确定应用将包含的内容和功能, 这将为我提供一个良好的结构框架。 这样你就能有条理地 进行开发,并知道如何使用顶部栏和工具 栏等导航组件。
我首先进行了头脑风暴,列出了我和团队希望 这款应用具备的所有功能和特性。
我们希望通过这款应用来庆祝 目标的达成,并且欢迎所有创意。 当时我们正处于头脑风暴阶段。
设计师说:“就一分钟。 ”随后,我进行了 简化,仅保留了旅行愿望清单应用的核心功能, 例如创建行程、添加照片、 搜索等;接着我退后一步, 将相关想法进行分组,比如行程、活动、 进度以及庆祝完成的行程。
这些组别代表了应用的三大板块,因此我将它们 命名为“愿望清单”、“目标”和“搜索”。
这就是我们的应用。这就是它对你的意义。
无论你是从零开始,还是重新审视现有应用。 进行这一练习都大有裨益。 我在开发者中心的工作坊中,与开发者们一起 进行过一段时间的此类练习, 每次都让人大开眼界。 因此,我鼓励你也尝试一下。 这有助于你理清思路, 将头脑风暴中那些零散的想法转化为相互 关联的界面,让你做好开发准备。
接下来我将解释这一切如何 与 SwiftUI 相关联。 我知道这就是你们来此的目的。 我马上就会在 iOS 部分讲到这一点。 有两个组件支持应用导航:顶部栏和工具栏。 首先,我将向
大家展示顶部栏的工作原理。
顶部栏显示应用的顶级板块, 并在所有屏幕上始终可见。
在这个示例中,应用 给人的感觉可能比实际要复杂。 我们都在这类应用中见过 这种情况:功能越多,元素就越多。 顶部栏总是不断弹出,但很少有人喜欢这样。 界面会变得非常复杂。 因此,我始终建议将标签数量控制在较少范围内。
这有助于保持界面简洁,且使用体验非常直观。 它能减少用户的决策负担。 所以每当我打开应用时, 都能清楚地知道有哪些选项可用。
其次,请保持标签栏的原生样式。 这在你的今天“宾果卡”里了吗?
正如你们所知, SwiftUI 组件(比如标签栏)自带动画 和无障碍支持等内置行为。 如果我们进行自定义,这类行为很容易丢失。
但别担心,应用中还有 很多其他地方可以展现个性。 顶部栏只是其中之一。
最后,确保顶部栏使用清晰的图标和标签, 并与每个标签页内的内容相匹配。
“愿望清单”并没有通用的图标,对吧? 所以我选择了 一道彩虹,它既易于识别, 又能体现人们渴望实现的愿望,对吧? 就像它充满希望一样。
另一方面,“目标”这个标签 则采用了一个更直观的图标, 它不仅含义明确,与标签高度契合, 而且形状与其中的许多收藏品一致, 我觉得这是一个很棒的设计细节。
只要遵循这些指南, 你的标签栏既能高效又能有效。
现在我要稍微岔开一下话题。 如果你想更深入地了解设计或标签栏,
请查阅《人机界面指南》。 你之前也听我们提过这个。 《海牙指南》(The Hague )汇集了所有 Apple 平台 和技术的设计指导与最佳实践。
在向我们咨询之前, 这里是你寻求解答和建议的首选之地。 你可以在开发者网站的“ 设计”板块中找到《海牙指南》的链接, 该板块还包含极其 宝贵的 Apple 设计资源。
“愿望清单” 应用以及我设计过的几乎所有应用, 都采用了该库中的原生组件。
在浏览并构建 你的设计工具箱时, 请务必下载 SF Symbols 应用。 它堪称图标界的“苹果酒”。 其中包含超过 7000 个符号, 你可以直接复制粘贴到设计稿或代码中。
这些是我用于顶部栏的符号, 而且这绝不会是最后一次。 今天我还会提到它。 好了,回到正题。 顶部栏和工具栏都支持应用导航。 但两者之间是有区别的,对吧? 它们作用于不同的层级。 用户会使用顶部栏在应用中进行跨模块导航。 我们刚才提到的是顶级导航,对吧? 但当需要执行操作并进入特定板块时, 我们会使用工具栏。
工具栏包含多种元素来支持导航。 首先是当前视图的标题。 在这个屏幕中,你可以看到用户。
他们能清楚地知道自己身处 “愿望清单”中——抱歉, 是“愿望清单”这一部分。 他们对当前屏幕的内容也有一定的了解。 这就像是定下了基调。 然后,工具栏会显示 屏幕上最重要操作的控制按钮。
这里的主要操作是创建行程。 所以我将一个控制按钮放 在右上角来实现这一功能。
工具栏的第三个元素是导航控制按钮。 在这里的行程详情页面中, 我添加了“返回”按钮,让用户可以返回上一级, 重新回到“愿望清单”。
为了便于用户快速浏览, 我尽量减少了操作项的数量。 与顶部栏采用相同的引导原则, 当项目过多时,通过选择 熟悉的 SF Symbols 来确保
含义清晰。 显然,在这个示例 中,用户很难理解应该先做什么。 因此,为了解决这个问题, 我只需找出那些不常用或更高级的操作, 并将它们隐藏在“更多”菜单中。
工具栏还能为关键操作提供醒目的样式。 这不仅增添了一抹亮色,还能瞬间形成视觉焦点。 在设计应用时, 请确保每个屏幕仅对一项操作使用这种样式。 说到底, 我知道有很多突出功能我们 可能都想强调,但你知道, 如果所有内容都重要, 那就意味着没有内容是重要的。
所以在这个例子中,没有旅行 计划的“旅行愿望清单”还算什么呢? 所以我希望把它突出显示出来。
关于工具栏,最后我想说的是,就像标签栏一样, 最好保持原生样式。 我们一起说出来吧。
添加背景之类的额外元素其实——你知道的—— 会与你的内容产生冲突。 它会与功能、与过渡效果产生冲突。 稍后 Curt 也会稍微谈一谈这一点。
到目前为止,我主要 专注了应用的结构以及用户如何 在其中导航。 现在是时候将内容引入界面, 并决定如何呈现它们了。
这就是我所说的“布局”。
我将向大家展示两个我常用的布局模式—— 列表和集合——的示例。
这两种模式都非常灵活, 你们完全可以选择其中一种, 但很快你们就会发现,根据想要 展示的内容以及用户需要对内容进行的操作, 其中一种选项会比另一种更合适。
例如,当内容以文本为主时,我会使用列表布局。 没错。而且需要展示多个项目时也是如此。 这有助于用户快速浏览内容。 这就是为什么这种布局 非常适合“行程活动”模块。
这里我使用了名为“edge to edge ”的样式,因为有这个需求。 而当我需要对内容进行分类时, 我会使用“组”样式。 所以这只是细微的差别。 但“内边距” 布局和圆角设计,确实能有效区分列表 中可能包含的不同内容类别。 因此,我在应用中其实并不需要这种样式。 当你下载并开始尝试时, 是不会看到这个示例的,但我还是想向 大家展示我是如何实现的, 因为大家总是对“何时使用全屏布局、 何时使用内边距布局”这类问题感到困惑。 这就是我们的实现方式。
最后,你是否帮助用户快速浏览并操作列表? 我会使用辅助元素和控件。 诸如图片和副标题 之类的辅助元素,能帮助用户 更快识别项目,而无需逐行阅读; 像这里的操作按钮这样的控件, 则能让用户无需跳转到其他视图即可执行操作, 这在实际应用中非常常见。 有时应用会变得非常复杂, 原因在于我们为了 实现几个本可以在列表视图中直接 完成的操作,却额外添加了视图。
你可以添加多种类型的控件来支持 不同的功能。 因此,我建议大家去探索一下可用的控件, 看看是否有机会简化布局。 这里有步进器、切换按钮、 滑块等众多控件供您尝试。 以上内容主要涉及列表, 它能帮助用户快速浏览文本。
但当目标是浏览照片、视频或产品时, 集合(Collections)则更为合适。 SwiftUI 中的集合本质上是 由 StackView
和 ScrollView 组成的结构, 它允许内容延伸至屏幕之外, 并引导用户通过滚动来探索其中的内容。
但在“愿望清单”标签页中,主要目标是浏览。 因此,我选择构建 一个包含多种“集合”变体的布局,
让旅行内容呈现得更具视觉吸引力, 同时营造出个性化且令人兴奋的体验, 展示用户上传的照片。
请注意,使用“集合”时, 并非所有内容都能同时显示。 大部分内容都位于屏幕之外。 因此,为了让用户建立 正确的预期并鼓励他们滚动浏览, 我会确保有效地添加标题和图片。 这意味着什么?
在这个示例中,你觉得这些图片是否增加了价值?
看起来是不是有点随意? 是的。是的。非常随意。 这就是问题所在—— 有时我们会因此失去内容的公信力, 因为图片的排列缺乏逻辑。 看起来不像经过精心策划的。
然后我们可以添加长度一致的标题。 否则,布局看起来确实会有点笨拙, 而且对齐不齐。 这不仅会影响集合的整体效果, 而且当下方还有内容时, 垂直滚动体验也会受到影响。 现在一切看起来都有些摇摇晃晃, 所以为你的集合内容设定一些规则, 会让整体看起来整洁美观。
好的, SwiftUI 已经实现了我 在讲解中零星提到的许多设计原则,但诸如层级、 对齐和邻近性等概念, 都属于组件的范畴。 不过我们也鼓励大家开始探索这些概念, 以此来创建自己的布局。 这正是视觉设计的重要组成部分。
接下来,我们将利用文字颜色和图片来引导视线, 并展现你的个性。 到目前为止,我们一直侧重功能性,力求高效。 现在,让我们看看能否为设计 增添一点你的个人魅力。 因此,我将专注模式设定 为视觉设计中影响最大且最先 显现的三个方面: Textile 样式、 语义色彩和一致性。
它们共同使应用变得赏心悦目、易于理解, 这也是我非常喜欢的地方。 它们将为你提供指导,让你在应用 发展过程中重复利用这些设计模式。
我将从文本样式开始讲起, 它们确立了样式层次结构。 它们——抱歉——它们 确立了层次结构,并确保在不同 屏幕尺寸和条件下都能保持可读性。 因此,我将使用系统文本样式, 这样你就能开箱即用地获得这种层次结构。 但重要的是,你要根据应用中的具体 用途选择合适的 Textile。 我始终如一地使用这一原则。 例如,标题三(Title 3)用 于所有章节标题,比如“夏日光辉”。 这让每个屏幕看起来更均衡、更精致, 因为内容被划分为明确的章节。 每当我看到一种 Textile 并知道它代表哪个层级时, 我就能准确理解该内容的含义。
我依赖系统文本样式的另一个原因是动态类型。 很多人会使用更大的文本样式,因为这样更舒适, 有时两者兼顾。 而当你使用动态类型时, 这些文本样式虽然名称相同, 但尺寸会始终更大。 所以请务必使用它。
如果你是字体爱好者, 你会注意到我在整个应用中都使用了 Apple 系统字体 SF Pro, 但我显然并非仅使用一种样式。 SF Pro 包含多种变体,这让我能 在不牺牲可读性的前提下塑造应用的个性。 我们追求一种更具运动感的风格, 因此我在标题中使用扩展版来强调, 而在某些叠加层中仅少量使用紧缩版。
同样地,我希望将选择保持在最小范围内, 这样既能让 UI 显得经过深思熟虑, 也能让应用更容易维护和扩展。 接着我们回到纺织品主题, 就会产生这样的疑问:哦, 什么时候该用扩展版? 什么时候该用 Kona? 现在我们有了大量的纺织品、大量的变体,天啊, 还有一大堆字体。
所以之前在解释“ 集合”时,我提到图像承载了视觉分量。 因此,语义色是用户界面的良好补充, 它们扮演着非常相似的角色,但更为具体,
即用于状态和反馈。 例如,我在整个应用中都 使用了靛蓝色(Indigo)。 比如这里, 它出现在已完成的任务中, 以及顶部栏中选中的标签页上。
我避免将它或任何与靛蓝 非常相似的颜色用于装饰。 否则用户可能会困惑:这部分是可交互的吗? 它有什么含义吗? 我将这种颜色命名为“靛蓝”。 在此之前,我们正在描述语义颜色。 所以它并不是那种随处可见的颜色。 是的,我是随机选的。
这些颜色由系统提供,每个原生组件——抱歉, 每个原生组件都有自己的颜色。 所以我没有设置背景色。 也没有设置分隔线颜色。 这些对我来说都是开箱即用的。 我唯一选择的就是这个强调色。
因为这些颜色会自动适配浅色和深色模式, 以及“液态玻璃”效果和不同的屏幕环境。 最好不要在自定义方面再次走得太远, 尤其是按钮和控件。
你们可能已经在应用中注意到, 配色方案中还包含一种霓虹绿, 我只将其保留用于强调效果, 比如进度指示器和副标题。 再次强调,这是为了传达 含义并运用这些视觉隐喻。 因此,这种 颜色不具有语义含义。 你们在我们的配色库中找不到它。 因此,我确保它具有足够的对比度。 我已在各种不同的背景上进行了测试。
我还提供了一个用于提高对比度的数值。 所以,如果你决定使用非系统颜色, 只需确保遵循一些简单的规则,就像这一条一样。 持续测试,并确保你的应用依然富有表现力, 同时不影响可用性。
现在我快要结束了, 这意味着是时候确保一切看起来都尽善尽美了。 作为最后的润色, 我会回过头来确保所有元素对齐 且间距一致。
这会让设计显得更加精致,视觉上也更协调。 这样不仅看起来 专业,还能帮助用户更好地 接收信息并决定下一步行动。 如果大量组件像被挤在狭小 空间里一样,会发生什么? 我们会感到压力,仿佛没有思考的空间或时间。 而当我们让界面“呼吸”得更舒畅一些时, 卓越的用户体验便由此开始—— 这正是为用户留出思考空间的关键。
因此在使用原生组件时, 我会注意以同样的方式使用它们, 并在不同屏幕上保持一致。 我会避免给用户提供多种完成同一操作的方式—— 这种情况有时确实会发生。 我们往往会兴致勃勃地创建新界面, 并从零开始设计。 但这其中最棒、最棒的一点,不仅在于简化开发, 更在于用户将获得的体验— —那就是复用这些组件。 用户知道这些组件,知道在哪里见过它们, 也知道它们的行为方式。 而且他们完全清楚系统对他们的期望。 例如进度指示器, 这里使用了相同的强调色和相同的形状, 但尺寸还是稍微小了一些。 它们出现在我熟悉的上下文中。 是的。它们…… 它们起什么作用?这种一致性也有助于开发工作。 你需要构建的组件更少。
我在讲解“系列”时简要提到了图像, 但它们当然也是视觉设计的一部分。 因此,在选择图片和插图时, 将它们并排摆放,观察它们在排版、 细节程度和整体氛围上是否协调。 然后你可以进行微调和调整, 让它们看起来像是同一个品牌的一部分。 如果它们感觉属于同一个系列, 那就听从你的直觉。 你的直觉很可能没错。
以“愿望清单”为例,用户会上传自己的照片。 但为了进一步说明这一点, 我使用了图片向大家展示 如何在代码中实现这一效果, 你将能够访问这些图片。 所以你可以看到我们之前探讨过的这种模式。 因此我选择了“动态天空”方案。 通常主体较小,或者与空间产生互动, 但核心理念是让 UI 比照片更突出—— 不过这确实需要你自行评估, 以确保应用风格的统一性。
回过头来看,我确实可以为这款 应用的设计这款应用的设计,我确实功不可没。
不过, SwiftUI 其实帮 我处理了大量设计决策。 说实话,这确实是事实。
但这是真的。我非常享受与工程 师们合作开发这 款应用的过程,看到我设计的一切 都能在代码中得到高度还原, 这种感觉太棒了。 因此,我们非常兴奋能与大家分享这款应用, 并期待大家亲自体验。 希望你们能从这些技巧中汲取一些灵感, 应用到自己的工作流程中, 从而减少一些不确定性——你们知道的, 这种不确定性有时会在想要发挥创意时出现, 比如在某个随机时刻, 或者只是在项目初期为自己 留出一个发挥创意的空间。 做出决策,头脑风暴,然后开始编码。
因此,当你们重返工作时, 请定义或评估应用的结构及其导航逻辑。 有意识地使用组件。 这将帮助你做出清晰的决策。 这样,当你开始编码时, 你就完全清楚自己的方向。 你完全清楚自己要构建什么。 没错,你将打造出一个更具深意的应用。 好的,所以请与 SwiftUI 携手合作,让它承担繁重的工作, 然后尽你所能为应用注入个性、 创造力和人性化元素。非常感谢大家。 希望你们在开发过程中乐在其中, 我也在试用这款应用, 关于设计方面的内容就到这里。 感谢大家的参与。 现在我将话筒交还给莉亚。
哇,太棒了。 马约,我特别喜欢 SwiftUI 系统组件 能让我们轻松设计出用户一目了然、 轻松上手的应用。 “愿望清单”是一款非常精美的应用, 花点时间停下来细细品味每个设计 决策背后的考量,让我更加珍视它。 无论是在制作全新的原型,还是优化现有应用, 我总是会思考如何充分利用系统组件。
好了,现在我们先稍作休息。 太平洋时间 11:15, 我们将在此重新聚首, 进行一场关于 SwiftUI 布局的演讲。 到时候见。
欢迎回来。
希望大家在休息 期间过得愉快,有机会享用点心或伸展一下身体。 现在,我的同事 Cat 将为大家介绍 SwiftUI 中的布局指南。 请和我一起欢迎 Cat 上台。
大家早上好。大家好。 我叫 Cat, 是 Apple 公司 开发工具工程师。
过去几年里,我 参与了 Apple Watch 上的 Vitals 应用, 以及 iPhone 和 iPad 上“健康” 应用中的睡眠体验等项目的开发。 没错,这些体验必须让人感觉直观且美观。 无论您是瞥一眼手腕,还是低头看手机。
通过这些工作,我了解到扎实的 UI 布局基础是实现这一切的关键。 今天,我将介绍 SwiftUI 布局中那些通用的核心原则, 无论您在哪个平台上开发。
首先,我将回顾 SwiftUI 中一些对布局 尤为重要的概念性组件,
随后介绍 SwiftUI 的布局机制。
接着,我将通过示例向大家 展示如何使用 SwiftUI 获得可预测的结果。
那么,我们就从基础开始吧。 关于视图,正如 Leah 今天上午所描述的, SwiftUI 视图是一种轻量级模板, 用于描述屏幕上将显示的内容。
视图是一种模板, 用于表示位置、大小和层级结构。
SwiftUI 会创建轻量级的视图 结构体来描述屏幕上的内容, 并在后续更新时丢弃这些 结构体并生成新的结构体。
SwiftUI 的更新引擎仅 更新发生变化的 UI 部分, 从而确保根据状态变化获得 可预测且高效的渲染结果。
这种效率源于 SwiftUI 的局部更新模型。 视图层次结构中某一 部分的更改不会波及无关的子树, 因此只有受影响的子视图会被重新渲染。
在 SwiftUI 中, 视图模板控制着从定位、 尺寸到渲染及状态更新的一切。
既然已经介绍了什么是视图以及 SwiftUI 的更新引擎如何工作, 接下来我将展示当 SwiftUI 进行布局时, 这些概念是如何结合在一起的。
在讲解布局机制时, 我将专注介绍我在“愿望清单” 应用中构建的 TripCard。
TripCard 很好地 展示了 SwiftUI 如何根据不同的状态处理动态布局。
而 SwiftUI 的布局正是由状态决定的。 这里所说的“状态”, 并非指 `at` 状态属性包装器, 而是泛指一般意义上的状态, 例如行程的名称(
如这里的“Cali Coastal Trails ”),
或是行程图片的 URL。 SwiftUI 本身就将状态 与布局进行了抽象分离。
虽然其他 UI 框架也能实现这一点, 但这需要付出一定的努力并保持严谨。 而这种抽象是 SwiftUI 不可或缺的一部分。
状态通过动态控制 UI 来驱动布局。
状态还会填充每个视图中的内容, 例如屏幕上显示的字符串和图片。
在其他框架中,状态变化需要手动 更新才能重新布局UI的特定部分;
而 SwiftUI 中的状态变化会自动 重新生成受影响的视图模板, 始终确保其保持最新状态。
接下来,我将讨论如何为“愿望清单”应用 中的“行程”卡片构建布局(如图所示), 以此说明如何将布局从状态中抽象出来。
“行程”卡片显示在纯色的深色背景上。
它包含一个 VStack, 其中包含一张代表该行程的图片
以及一个显示行程名称的文本视图。
“行程”图片视图上叠加了 一个显示活动数量的覆盖层,
但仅当活动数量大于零时才会显示。 否则,该文本将不会被创建, SwiftUI 也不会渲染活动覆盖层。
视图的状态由模型中的“行程”独立控制。
“行程”模型包含一个用于行程 标题的名称字符串和一个照片 URL。 用于确定应为该“行程”渲染哪张图片。
最后,它还 包含一个包含 各项活动的字典。
字典中活动的数量用于决定是否 显示活动叠加层。 这是一个关于状态 如何控制 SwiftUI
布局以及值如何影响布局的简单示例。
在此示例中,活动的数量被用于自 定义视图的布局。
接下来,我将解释 SwiftUI 中布局的工作原理。
SwiftUI 布局的基本前提是: 容器会向其子视图提出一个尺寸建议。
该建议尺寸受容器
视图及其所有上级 容器视图的约束。
每个子视图会通过计算并 反馈其首选尺寸来响应该建议。 不同的视图和 SwiftUI 本身具有不同的首选尺寸。
颜色视图 总是占用所 分配的全部空间。
它会扩展以填满可用空间。 ImageView 总是占用其完整尺寸, 即使分配的空间较少也是如此。
这意味着图像可能会超出其容器视图的边界。 使用 resizable 修饰符可以覆盖此行为。
这样,它就会像颜色视图一样占用所分配的全部空间,即使最终看起来像这里的沿海花朵那样 被挤压变形。要保留图片的原始宽高比,请将内容模式设置为“fit”或“fill”。 文本至少会占用足够显示省略号的空间。
文本只会占用其内容所需的空间。 布局和 SwiftUI 是按层次结构进行的。
容器视图会向其每个子视图提出一个尺寸建议。 假设该建议尺寸是具体的。 例如,这个橙色方框展示了建议尺寸可能的样子。 如果容器视图是应用的根视图, 则建议尺寸即为设备尺寸;或者,如果你 应用了帧修饰符,建议尺寸即为该帧的尺寸。 另一方面,有时建议尺寸在某个或两个方向
上未指定。
这意味着容器视图希望了解: 如果子视图完全不受约束,它会需要多少空间。 这就是子视图的理想尺寸。
每个子视图会根据建议尺寸,返回其首选尺寸。
与其他视图系统相比, 这是 SwiftUI 的一 种特殊且独特的行为。 建议值不仅会向下传递,也会向上传递。
这个布局过程是递归的。
建议值从根视图开始, 沿整个视图层次结构向下传递。
随后,响应值会向上回传。
获得尺寸信息后, 根视图会告知所有子视图应绘制的位置, 子视图再告知其子视图, 如此循环,直至所有视图绘制完成。
现在,我将再次带大家体验这一遍遍的遍历过程。 这次,我们将在这里的 TripCard 中使用一些实际的视图和实际的数值。 TripCard 出现在一个水平 ScrollView 内的水平堆栈中。 我希望所有 TripCard(如 Kali、 Kyoto 等)都具有相同的高度,
无论源图像的大小或行程名称的长度如何。 我可以通过使用 固定高度的框架来实现这一点。
SwiftUI 为 TripCard 根节点
处的 VStack 提供了无限宽度和220的固定高度。
随后,该堆栈将220的高度 传递给 Trip 图像视图。以下仅是一个固定 高度框架的示例。
图像视图返回其确切所需尺寸 : 185 × 150 点。
外层堆栈减去返回的高度, 然后将剩余空间分配给包含 “Trip”名称的文本。
名称文本返回其所需的空间 : 15 × 130 点。 我已经到达最后一个视图。
现在,每个视图都已返回其所需的尺寸。
这里指的是除“活动”叠加层之外的每个视图。 我将在演示的后半部分再回到这个叠加层。
完成该过程。 VStack 将内部图像、 视图和文本的请求尺寸合并, 并返回该视图的最终请求尺寸。
这就是 SwiftUI 布局的工作原理。
总结一下, SwiftUI 布局是 从上到下遍历的, 尺寸信息会在任何层级向上回传至根视图。 可用空间由容器视图提供。
所需空间由子视图确定, 可能需要通过检查其自身的子视图来确定。
只有当状态发生变化时,布局才会重新计算,
而视图模板则会始终被重新创建。
状态控制着内容, 因此当“Trip”值发生变化时, 视图会自动更新。
太棒了。现在是时候创建一些令人 惊艳的 SwiftUI 视图了。
当我刚开始接触 SwiftUI 时, 有时构建视图后却得不到预期的结果。 因此接下来,我将分享一些指南, 通过保持布局简单且可组合, 从而获得可预测的结果。
实现 可预测 布局的首要
考虑 因素是 布局优先 级。 布局优先级用于确定视图在间距调整 和尺寸调整时的排列顺序。
当可用空间不足空间无法满足 视图的需求时,系统会将更多 空间分配给布局优先级更高的视图。 布局优先级通过 layoutPriority 修饰符设置,其默认值为零。数值越高,优先级越高。例如,优先级为3的视图 优先级将高于优先级为1的视图。这可能与你在缺陷跟踪系统中习惯的情况相反——在那里, p1 通常代表最高优先级。 但在这种情况下,情况恰恰相反。
数值越大,优先级越高。 以“愿望清单”应用为例。 我创建了一个“旅行”板块, 其中的卡片宽度更宽。
我与“愿望清单”应用的设计 师 Majo 交流后, 我们认为这些宽版行程卡片是充分 利用额外空间、同时显示 行程副标题和创建日期的绝佳位置。
有时,当一个容器视图中包含多个文本视图时, 文本可能会根据可用空间被截断或换行。
在这个示例中,一旦 我添加了副标题和日期, 卡片左下角的“夏威夷美国”文本就会被截断。
我可能无法确定可用空间有多少。 例如,不同尺寸的设备上可用空间可能不同,
而且我可能不知道自己需要多少空间。 有些行程的副标题字符串较长, 因此通过在布局中标注不同文本的重要性, 你可以自主决定截断哪部分文本。
你可以通过将文本的布局优先级 从默认值 0 提高到 1 等更高 数值来实现这一点。
正如我之前提到的,堆栈会优先 为优先级更高的视图分配空间,
因此“行程”副标题能获得所需的全部空间。 但这会导致右下角的日期文本被截断。
我将这个布局问题反馈给了 我们的设计师 Majo。 她建议在空间不足时隐藏创建日期, 这样“行程”副标题就能完整显示。
我认为这是一个很好的建议, 因为它避免了字符串被截断。 现在我将演示如何实现这一点。
我将 HStack 移到了 ViewThatFits 视图内部。 ViewThatFits 会评估其子视图。 评估顺序与你提供给初始化器的顺序一致。
它会选择第一个理想尺寸( idealsize)能适配给定大小的子视图。 这意味着你可以按优先级顺序提供视图。 通常这个顺序是从最大到最小。
所以我添加了一个仅包含副标题的文本视图。 当 HStack 中的视图即将被截断时, ViewThatFits 会选择该文本视图。
我相信这些改动会让 我们的设计师 Majo 很满意。
接下来是堆栈的对齐方式。 堆栈会将视图排列成对齐方式一致的布局。 默认对齐方式是居中, 但也可以指定其他对齐方式。
除了居中之外, HStack 中的视图还可以按顶部或底部进行垂直对齐, 也可以对齐到 firstTextBaseline 或 lastTextBaseline。
VStack 中的视图还可以按前缘、 后缘等方式进行水平对齐。
“愿望清单”应用中“旅行 ”收藏夹的章节标题就是一個完美的示例。 默认情况下,堆栈使用居中对齐来排列视图,
但有时当有一系列元素时, 这可能会显得对齐不齐。 在这种情况下,最好将它们全部对齐到基线。
在此示例中,“章节副标题”的子 视图使用了默认对齐方式。 这意味着“显示全部”浮动在副标题 文本所确定的视觉基线上方。
通过将 HStack 的对齐方式改 为 firstTextBaseline, 我将“显示全部”提升到了 与副标题所确立的同一视觉线上。
现在,我将分享一些在 SwiftUI 中构建布局时需要考虑的要点。
在可能的情况下,请创建自 适应布局,而不是显式布局。 自适应布局能让你更轻松地适配不同的设备尺寸、 窗口尺寸和平台。
避免显式操作视图帧,不要为视图设置 显式的高度和宽度。 让它们自动扩展以填满可用空间。
仅当无法通过自适应、 灵活的方式实现所需布局时, 才使用 frame 或 position 等视图修饰符。
类似于 ZStack。 若要为视图增添深度, 可以使用 background 或 overlay 修饰符。
overlay 和 background 修饰符不参与布局尺寸计算, 其尺寸始终与被修饰的视图相同。
这就是为什么在之前的布局树遍历示例中, Activity 的 overlay 未被纳入布局计算的原因。 它继承了其容器视图的计算尺寸。 默认情况下,图像视图也是如此。
布局问题在所难免。 我至今仍会偶尔遇到这类问题。 接下来,我将分享一些我用来识别 和修复这些布局问题的技巧。
其中许多技巧都基于 SwiftUI 在快速原型设计方面的强大功能。 我只需稍作调整,结果就会立即显示出来。
一种简单的方法是在图像上添加红色边框、 在文本上添加蓝色边框, 就像我在这里的 TripCard 中所做的那样。
这种方法对于理解堆栈布局以及 视图周围的填充特别有用。
有时临时边框会重叠, 而我希望更直观地展示子视图的大小和位置。 这种情况下,我会使用半透明颜色的叠加修饰符, 以完整展示子视图的范围及其层叠关系。
在视图渲染时打印属性值也十分有用。
你可能曾尝试使用 `print`, 却因编译器报错而感到失望。
此处的错误提示为“buildexpression 不可用”。 该表达式不符合 `View` 协议。
出现此错误是因为 Swift 中的 `print` 表达式没有返回值,
因此其返回类型为 `void`。 而 `void` 并非视图,但视图主体确实支持变量声明。
我可以通过声明一个占位符变量来修复此错误。 现在,我可以在等号右侧编写 `print` 表达式了。 这样编译时不会报错, 运行时也会将输出打印到控制台。
接下来, Xcode 会预览预览画布, 而 Xcode 提供了一种快速查看 哪个视图与哪个代码对应的方式。 点击画布底部的鼠标指针图标, 使预览区域可选中。
然后,当我在编辑器中选中代码时, 预览区域会高亮显示相关的视图; 反之,如果我在预览区域中选中某个视图, Xcode 会选中编辑器中对应的代码。
在我用于调试布局问题的工具箱中, 最强大的工具是 Xcode 的视图调试器。 通过运行应用并使用视图调试器暂停它, 我可以展开所有视图, 然后选择单个子视图进行重点检查。 当我将 SwiftUI 与 UIKit 混合使用时,这种方法特别有效。 这让我想到一个重要问题。
正如 AllTrails 的 James.Graham 稍后将分享的那样, SwiftUI 与 UIKit 布局兼容。 若想进一步了解, 请观看 WWDC 22 的视频。 将 SwiftUI 与 UIKit 结合使用。
既然我已经分享了 SwiftUI 布局的基础知识, 现在是时候将这些概念付诸实践了。
学习的最佳方式就是开始动手构建。 我最初的做法是从自己 喜欢的一个应用中挑选一个视图, 然后用 SwiftUI 重新实现它。
Xcode 预览功能是快速 尝试不同布局和方法的绝佳 途径。
利用打印和颜色 叠加等简单技巧,可以诊断遇到的任何布局问题。 你会惊讶于自己能如此迅速地发现并解决问题。
感谢大家今天参与。 现在,去用 SwiftUI 打造一些特别的东西, 并享受接下来的会议内容吧。
太棒了。Cat。 我特别喜欢那个关于如何积累布局 经验的建议:尝试重现现有视图, 多加实践,从而构建自己的思维模型。 接下来,我的同事 Curt 将深入探讨 SwiftUI 中的动画细节。 请大家和我一起欢迎 Curt 上台。
谢谢, Leah。
大家好,我是 Curt, 是全球开发者关系团队的技术布道师。 今天能与大家见面,并欢迎所有 在线的观众,我感到非常激动。 在加入开发者关系团队之前, 我曾担任 SwiftUI 工程师, 专注导航和 API 设计。
今天,我将探讨如何利用 SwiftUI 在应用中实现动态效果的基础知识。
随着设备性能的提升,用户界面也变得更加动态。 如果设计得当,这种动态效果 能让设备显得更加生动且富有个性。 动态效果让应用和平台更易于理解。 它能提供上下文信息,并引导用户的注意力。 此外,它本身就充满乐趣。
要实现这些动态交互,需要多方面的技术协同。 其中包括用户从一个视图切换 到另一个视图时的过渡效果, 例如在“设置”应用中滑入某个视图, 或是弹出专注模式选项面板; 还有用户与设备直接交互的手势, 例如放大地图以查看更多细节, 或在应用之间滑动切换。
最后,还有屏幕上的对象移动、 放大或改变视觉属性的动画, 例如“灵动岛”中的实时活动。 所有这些元素协同工作, 共同打造出流畅、交互式界面。
SwiftUI 是将流畅动态 效果融入应用的最佳方式。
SwiftUI 中的动态效果涵盖了方方面面。 最简单的情况是,只需使用 内置的 SwiftUI 组件, 即可获得自动平移过渡效果。
进阶使用状态驱动的动画, 你可以从修饰符菜单中进行选择, 告知 SwiftUI
在特定属性发生变化时如何进行动画处理; 或者直接采用完全自定义的动画, 精确指定每一帧。
今天,我将带大家了解涵盖这一全范围的示例, 以便您选择合适的工具, 为您的应用打造恰到好处的动态交互体验。
许多 SwiftUI 视图会自动 提供平滑的运动效果。 我将分享“愿望清单”应用中的几个示例, 并演示如何调整这些自动效果, 以在应用中营造您想要的氛围。 随后,我将探讨如何利用 SwiftUI 修饰符和属性, 为应用添加基于状态的动画。 我将介绍这些动画的基础原理, 以便大家能够针对具体任务选择合适的技术方案。 最后,我将演示 SwiftUI 中自定义动画的强大功能, 并为您推荐一些深入学习的资源。
当您在 SwiftUI 中使用内置视图时, 会自动获得许多动态效果。
这款“愿望清单”应用就是这一强大 功能的绝佳示例:当用户轻点“旅行”缩略图时, NavigationStack 会 进行推送操作。 勾选标记等符号会反馈交互操作。 详情会弹回缩略图中,弹出视图会动画显示, 下拉菜单会从“Liquid Glass ”按钮中渐变弹出。 滚动视图会在用户滑动时自动加速, 停止滑动时减速, 并可选择在特定视图处停顿。
我将介绍四种不同的 SwiftUI 视图, 你可以通过它们自动获得这些动态效果。 首先是导航堆栈,例如“愿望 清单”标签页中用于展示行程详情的这个。
该“愿望清单”应用在每个标签页中都 使用了一个 NavigationStack。
在这个堆栈内,包含着代表 各种行程的所有这些卡片。 我将专注模式应用于一个 for each 循环, 它会遍历集合中的所有行程, 例如我的“法尔度假”行程。
每个缩略图都由一个导航链接绘制而成, 该链接提供一个目的地视图以及一个标签。
标签是用户点击的界面部分。 当用户点击时,目标视图会被推入堆栈。 默认情况下,它会从尾部边缘滑入。
当用户点击返回按钮或在屏幕上滑动时, 目标视图会以动画效果淡出。 现在,像“音乐”和“新闻”这样的应用, 会通过一种称为“缩放过渡”的技术来优化这些“ 压入”和“弹出”操作。
只需三个步骤,即可让 NavigationStack 使用缩放过渡。
首先,在视图的属性中添加一个命名空间。 命名空间是一个唯一标识符, SwiftUI 可以利用它将两段代码关联起来。
其次,向目标视图添加 navigationTransition 修饰符, 并将 zoom 参数、 唯一 ID 以及命名空间作为参数传入。
最后,在链接的 label 上添加 matchTransitionSource 修饰符。
现在,当我点击“行程”的图片时, “愿望清单”应用会使用缩放过渡效果。
当我返回时,详情视图会缩小回缩略图。
当目标视图的内容 与标签中的内容重复时, 缩放过渡效果表现尤为出色, 比如这张来自锡安国家公园的美丽照片。
有时 NavigationLink 的标签内容更为详细, 例如“愿望清单”应用搜索标签页中的这个。
它在行程名称旁边显示了一张缩略图。 在这种情况下,请将“匹配的过渡源 ”修饰符添加到链接标签的子视图上。 以下是使用名为 SearchItemView 的自定义视图绘制这些行的方式。 它使用了一个包含缩略图 和行程名称的 HStack。
这里,我将匹配的过渡源修饰 符添加到了缩略图上,而不是整个搜索项视图。
我将命名空间作为属性从周围的 NavigationStack 中传递进来。 现在,当我点击该行时, 详情页会从缩略图中缩放出来。
当我返回时,页面会再次缩放回缩略图。 我特别喜欢这个小细节。 它既有趣,又能将用户的视 线引导回最初点击的那一行。
今天我要介绍的第二种内置视图 是弹出式视图(sheets)。 我将通过“愿望清单”应用 中的 Sheet 代码进行讲解。 这里再次展示“愿望清单”标签 页的 NavigationStack, 其中包含一个名为 isPresenting 的属性。 我将使用“添加行程”来判断 Sheet 是否正在显示。 栈内部是一个附带工具栏修饰符的滚动视图。
工具栏内包含这个加号按钮。
在该结构就位后,展示弹出视图只需 两个步骤。首先,工具栏中的按 钮将 `isPresentingAddTrip` 属性设置为 `true`;
其次,弹出视图修饰符指定 当 `isPresentingAddTrip` 为 `true` 时应显示的内容。
此处的修饰符接受该属性的绑定。 这就是美元符号的含义。 Cole 稍后会详细讲解绑定,但目前关键点 在于:当用户关闭该弹出视图时, 这能让 SwiftUI 将 `isPresentingAddTrip` 重置为 `false`。
配置完成后,当我点击按钮时, 弹出视图会从屏幕底部向上滑出。 当我点击关闭按钮时,它会滑回屏幕底部。
就像导航过渡一样, 我可以应用三步流程将其转换为缩放过渡。 第二步。 首先,我在视图的属性中添加一个命名空间。 其次,我为表单的内容添加 navigationTransition 修饰 符,并传入 zoom 参数。 最后,我为按钮标签添加匹配的 transitionSource 修饰符。
现在,当我点击按钮时, 表单会从按钮中逐渐显现出来。
当我关闭表单时,它会逐渐变回原状。
这种动态效果有助于用户将按钮与表单建立关联。
SwiftUI。 ScrollView 还提供了内置的动态效果。 当用户在 ScrollView 上滑动时, 其视图会随着滑动而移动, 并伴有平滑的减速效果, 在内容末端还会出现友好的回弹效果。 这是“愿望清单”应用中用于显示我已 获得的徽章的 ScrollView。 ScrollView 接受一个子视图, 该子视图显示用户可以滚动浏览的内容。 这里是一个由展示我所有徽章的图块组成的堆栈。 此外, ScrollView 还可以接受 一个参数来指定其可滚动的方向。
例如,对于“愿望清单”应用 中的徽章,我希望 ScrollView 在水平滚动时, 始终停在徽章居中显示的位置。 只需两个步骤,我就能让 SwiftUI 实现这一效果。 首先,我添加“scroll target ”行为修饰符,告知 SwiftUI 需对齐到某个视图处停止滚动。 然后,我添加“scroll target ”布局修饰符, 告知 SwiftUI 希望 哪个视图堆栈参与此行为。
设置完成后,现在当我滑动徽章时, ScrollView 总会在徽章 居中显示于屏幕时停止滚动。
现在可以玩点花样了。 如果能在这些徽章在视图中进出时, 为它们增添一些奇思妙想,那就更有趣了。 像“书籍”和“体育” 这样的应用就使用了这类动态效果。 它们是通过 `scrollTransition` 修饰符实现的。
将该修饰符应用到你想要进行变换的视图上。 这里,就是我已实现的目标磁贴。
我让过渡效果具有交互性, 使其响应手动滚动,并将方向设置为水平。
该修饰符接受一个闭包。 定义过渡效果。 SwiftUI 会将内容的代理 以及一个动画阶段传递给闭包。
该阶段指定了过渡进程的进度: -1 表示已完全滚出屏幕(前缘), 0 表示完全显示 在屏幕上, + 1 表示已完全 滚出屏幕(后缘)。
利用该阶段值来修改内容。
这里,当目标图块移出屏幕时,我会将其缩小,
并沿垂直轴应用 3D 旋转。
来看看效果。注意视图在边缘处似乎会环绕显示, 仿佛是圆形轮播图的一部分? 这效果很棒。
今天我要介绍的最后一种内置视图是 使用 SF Symbols 的图像 视图。
正如 Majo 之前所说。 SF Symbols 是一个包含 7000 多个符号的图标库。 此屏幕上没有重复的符号。
SF Symbols 旨 在与 Apple 平台上的系统 字体 San Francisco 无缝集成。
可在 Apple 开发者网站上获取的 Mac 版 SF Symbols 应用, 允许你浏览图标库并为你的使用 场景找到合适的图标。 例如, Wishlist 应用在其 活动列表中使用了一个复选标记,
我也可以在我的应用中添加这个复选标记。 我在 SF Symbols 应用中将 一个 SwiftUI 图像添加到代码中。 我对所需的符号(勾号、实心圆)进行控制点击, 然后选择“复制名称”。
接着在 Xcode 中, 将名称粘贴到图像声明中,
勾号就出现了。 SwiftUI 提供了多种 为 SF Symbols 添加动画的效果。 我将分享三个示例。
使用持续效果来表示正在进行的操作, 例如在聊天中等待对方回复。 这些效果在活动期间会持续运行。 一些示例包括变量、颜色、缩放和呼吸效果。
要将这些效果之一添加到 SF Symbols 符号中,请使用带有 IsActive 参数的符号效果修饰符。 只要该参数值为 true, 效果就会运行。
使用离散效果来表示下载等事件。 正在完成。此类效果在事件发生时触发。 例如弹跳、脉冲和抖动效果。
要将此类效果添加 到 SF Symbols 上, 请使用带 value 参数的符号效果修饰符。 每当该参数值发生变化时,效果就会运行。
在用一个符号替换另一个符号时 (例如在圆圈中添加复选标记), 请使用 contentTransition 效果。
要将此类效果之一添加到 SF 符号中:请使用带符号效果类型的 contentTransition 修饰符。 每当选定的符号发生变化时,过渡效果就会运行。 SF Symbols
应用还允许您尝试各种动画效果。
这里我切换到“动画检查器”。 选择一个“呼吸”动画并设置为运行状态。 点击“播放”进行测试。 然后通过“复制”菜单复制该动画配置。
回到 Xcode 后,我将该修饰符粘贴进来, 并应用在 SF Symbols 应用中配置的选项。
即可实现呼吸效果。
SwiftUI 内置了堆栈、 片状视图、滚动视图和图像等视图, 为您提供了多种内置效果。 自动添加修饰符,以优化应用中的这些效果。
接下来,我将介绍 SwiftUI 中的另一种动画类型:状态驱动动画。 我来详细说明一下。 之前, Leah 分享了 SwiftUI 视图如何将数据映射到屏幕像素上, 而 Cat 则演示了一个示例: 在“愿望清单”布局中, 根据数据模型中某个属性的值, 视图会显示或隐藏。
以这种方式使用的数据被称为视图的状态。
以下是“愿望清单”应用中的一个示例。 这是一个行程详情视图, 底部列出了我计划的活动。 这张图片上方悬浮着一段文字, 显示我已完成活动的百分比, 还有一条进度条, 随着我完成更多活动而逐渐填满。
当我勾选一项活动时, SwiftUI 会更新相关状态, 以记录该活动已完成。 依赖此状态的视图会在状态变化时自动更新。 目前,当我勾选某项活动时, 所有变化都是瞬间发生的。
接下来,我将让 SwiftUI 对这 一过程进行动画效果处理。 这可以通过 `with animation` 函数来实现。具体方法如下。 这是用于显示该按钮的代码。 该按钮 代表“行程详情”页面上的某项活动。 该按钮的操作会切换关联活动的“已完成”状态。
我将此切换操作封装在 `with animation`
函数中。 这会告诉 SwiftUI, 对因封装状态 变化而产生的任何视图更新进行动画处理。 再次展示更新效果。 现在,当我完成一项活动时, 百分比文本会淡入淡出,进度条会填满, 活动颜色和勾号也会淡入淡出。
为了实现这些变化的动画效果, 我只需添加 `with animation`, 其余操作便会自动完成。
`with animation` 的 强大之处在于其简单性。 只需将状态变化包裹在该函数中, 视图中所有依赖该状态且可动画化的属性 都会自动应用动画效果。 另一方面,应用中的某些部分 可能需要更精确的方式来描述这种变化。
我将专注“ 愿望清单”应用中的这个完成覆盖层。
在该应用中,这三个视图— —完成百分比文本、进度条和副标题—— 都嵌入在一个自定义的 `ActivityProgressView` 中。
为了专注模式,我将省略 几个样式修饰符以及副标题。 `ActivityProgressView` 接受 一个介于 0 到 1 之间的完成度属性, 用于描述完成百分比。
一个带格式设置的文本视图用于显示完成百分比。
当完成值为零时,不透明度修饰符会隐藏该文本。
我使用圆角矩形来绘制柠檬绿色的进度条。
进度条的宽度通过将常量条宽 乘以完成值来设定,同样,
当完成值为零时,不 透明度修饰符会隐藏该视图。
现在,“愿望清单”的设计要求 在任何使用 ActivityProgressView 的地方都对其进行动画处理。 我可以使用动画修饰符来实现这一点。
通过将动画修饰符应用于此 VStack, 我告诉 SwiftUI :每当 传入的完成度值发生变化时, 就对这些视图进行动画处理。
以下是目前的效果。 这与我使用 `with animation` 时得到的动画效果完全相同。
动画修饰符是 `with animation` 的替代方案。 当状态发生变化时。 当你希望状态变化的某些( 而非全部)来源触发动画时, `with animation` 是 一个不错的选择。
另一方面,当你希望视图 始终进行动画效果,无论状态 变化的来源如何时,请使用动画修饰符。 无论是使用“ with animation”还是“ 动画修饰符”,你都可以调整动画的时长、 速度、加速度和反弹效果。 利用这些选项,你可以更精细地 控制动画的呈现效果。
例如,如果进度条在增加时能稍微弹一下, 会增添一丝庆祝的氛围。 我可以通过将“bouncy” 参数传递给动画修饰符来实现这一点。 效果如下。 绿色进度条在稳定到新值之前会轻微地波动一下。
我甚至可以增加一些额外的弹跳效果。
看看现在会发生什么。 进度条有了有趣的弹跳效果。
不过…… 文字显示有点问题, 所以我再运行一次,并在动画过程中暂停画面。
我添加的额外弹跳动画会先超过目标值, 然后回弹到原始值附近,最后才稳定下来。 对于数字部分, 这种回弹幅度足够大,导致 数字开始淡化回初始值。 我可以通过添加第二个动画 修饰符来解决这个问题。
我仅对文本视图添加了一个平滑动画。
现在,弹跳动画应用于整个 VStack, 但我仅针对文本视图覆盖了该动画。 子视图。
现在数字显示的故障已经消失。 进度条在弹跳,但数字会平滑地淡入淡出。
既然这个问题解决了, 我将调整进度条,让它稍微弹回一点, 并为文本再添加一个运动效果。
带有数值文本参数的 contentTransition 修饰 符会告诉 SwiftUI, 当数字变化时使用一种特殊的计数动画。 现在,数字会按顺序向上滚动, 从而吸引人们对变化的注意。
我特别喜欢这个效果。
总而言之, SwiftUI 视图将数据映射为像素。 状态驱动的动画让你能够控制 当数据或状态变化导致像素更新时的行为。
您可以通过将实际的状态变化封装 在 `width` 动画函数中, 或者为视图添加动画修饰符, 来指定这些动画的行为方式。 在继续讲解之前, 我想简单介绍一下我用来控制 条形动画弹跳效果的可选参数。 这个可选参数控制着动画的时机。 我的意思是这样的:我 为条形动画使用了弹跳效果。 该动画会在回弹前先超过目标位置, 然后才逐渐回弹。 在“值随时间变化”的图表中,
这是内置动画中最富趣味性的一种。 数值会在达到最终水平前先出现超调。
另一方面,“利落”动画会快速移动到最终值, 给人一种精准且专业的感觉。
图表中显示了一个明显的拐点, 即数值达到最终水平的位置。
“平滑”动画在“弹跳”和“迅捷 ”之间找到了一个绝佳的平衡点。 它虽然也能快速稳定下来, 但比“迅捷”动画显得更随性一些。
它的曲线更平缓, 接近最终数值的过程也更为渐进。 “平滑”是一种非常优秀的通用动画效果。 事实上,它是 Apple 所有 平台上的默认动画效果。
最后,使用“弹簧”动画 可以自定义弹簧的所有属性。 这能让你进行精细控制。 调整“弹跳”参数可控制动画的活泼程度。 当“弹跳”值为 0.6 时, 数值会在最终稳定前多次振荡。 你还可以调整弹簧的持续时间, 甚至控制它与其他弹簧驱动动画的交互方式。
通常情况下,弹簧的预设模式“bouncy”、 “snappy”或“smooth ”中的一种就已足够, 但知道在需要时可以自行控制,这也很不错。
若想了解动画和 SwiftUI 在底层的工作原理, 请参阅《探索 SwiftUI 动画》。 然后跳转至《使用弹簧制作动画》, 获取有关动画时序的技术和设计指南。
这两段视频均来自 W 23。
我已经分享了 Wishlist 应用如何利用 SwiftUI 的内置视图 自动生成精美的动态效果, 以及该应用如何通过状态 驱动动画进一步提升视觉质感。
最后,我将通过几个使用 SwiftUI 构建的自定义效果示例来总结。
这些示例目前尚未包含 在 Wishlist 应用中, 但我会讲解如何将其添加进去。 通过这样的实验,可以很好地 体会 SwiftUI 的潜力。
我的第一个示例是一个弹跳的球。 这可以作为加载指示器使用。 使用 PhaseAnimator 来实现这个效果。
该动画包含四个阶段。 首先,球体下落。
然后它“挤压”一下——这是一个专业术语。
接着球体再次膨胀,然后上升。
这个循环会不断重复。枚举(enum )是定义 PhaseAnimator 各阶段的绝佳方式。因此,本示例中 定义的阶段为:下坠、压缩、膨胀和上升。 接着,为每个需要动画化的属性添加相应属性。 在本示例中,首先添加 y 偏移量属性。 返回每个阶段结束时的数值。 在上升阶段结束 时,球体比其原始位置高出 40 点。 在压扁阶段结束时,球体比原始位置低 5 点。
在其他两个阶段结束时,球体位于其原始位置。 接下来,为球体的缩放比例添加一个属性。
当球体被压扁时,球的宽度略微增加, 高度略微降低,与正常形状相比有所变化。
在定义完各阶段后,球体将恢复为圆形。 向视图中添加一个 PhaseAnimator。
通过让你的枚举符合 CaseIterable 协议,并将所有 情况传递给 PhaseAnimator, 使动画器遍历所有阶段。
SwiftUI 将依次 调用动画器的第一个闭包, 并依次传入每个阶段。 即下沉、收缩、扩张和上升。
接下来,在此处绘制带动画的视图。 这就是那个圆。然后应用修饰器, 从阶段中读取参数。 这里,我根据阶段的 y 偏移量进行偏移,
并根据阶段的缩放值进行缩放。
最后,在第二个闭包 中,定义每个阶段应使用的动画。 这里我为“下沉”和“展开”阶段 使用了 ease-in 动画。 这类动画起始缓慢、结束迅速; 而对于“挤压”和“上升”阶段, 我则使用了 ease-out 动画。
以上就是在 SwiftUI 中构建阶段动画的四个步骤。 为每个动画属性定义阶段。 定义每个阶段结束时的值。 绘制视图时应用动画效果, 最后为每个阶段指定相应的动画。
要进一步了解阶段动画器,请参阅。 深入探索 WWDC23 中 介绍的 SwiftUI 高级动画。我要分享的第二个自定义效果, 是《 Wishlist 》 中为庆祝用户获得徽章而设计的过渡效果。
这个效果很酷,但播放速度很快, 所以我再播放一次。
在 SwiftUI 中, 当你添加、移除或替换视图时, 可以应用过渡效果。
这个徽章过渡效果包含两个主要步骤。 首先,编写代码将一个视图替换为另一个视图。 这里,我将先显示徽章的灰度版本, 然后将其替换为彩色版本。
创建一个组,将替换前后的图像放入该组中。 使用 if 语句来显示彩色图像。 如果徽章已获得,则显示彩色徽章; 否则显示其灰度副本。 否则。现在将 `earned` 设置为 `true`, 就会用彩色徽章替换灰度徽章。
第二步是让这个替换过程动画化。
为此,向组中添加一个 `transitionModifier`。 这会告诉 SwiftUI, 当组内的视图被替换时,应用一个运动效果。 这里可以使用一些内置的过渡效果, 比如不透明度或缩放。 但要让徽章翻转,需要使用自定义过渡效果。
通过声明一个符合 transition 协议的结构 体来创建自定义过渡效果。
transition 协议只有一个要求 :必须包含一个 body 方法, 就像我之前分享的 scroll 过渡修饰符一样。 自定义过渡效果的 body 方法接受 一个 content 参数。 这是正在被添加或移除的视图的代理。 主体方法还会接收一个 phase 参数, 用于指示视图是正在出现还是正 在消失。
在主体方法内部, 以 phase 为条件对内容代理 应用特效。 phase 有 一个名为 isIdentity 的属性。 当视图可见时,该属性值为 true。 因此,要翻转徽章, 当 phase 为 identity 时应用 3D 旋转效果(无旋转);
否则,根据视图是正在出现还是消失, 分别应用正向或负向 180 度的旋转。
应用不透明度修饰符, 在翻转过程中逐渐隐藏或显示徽章。
定义好自定义过渡效果后, 将其实例传递给过渡修饰符。
现在,当我在 A 中的“ isEarned”动画块内切换状态时, 灰色徽章会动画淡出, 彩色徽章会使用自定义翻转过渡效果动画淡入。
若想进一步了解如何在应用 中创建自定义动态效果, 我推荐观看视频 :来自 web 24 的《使用 SwiftUI 创建自定义视觉效果》。 如需设计指导,请查阅 developer.apple.com 上《人机界面指南》中的“动态”章节。 在考虑应用中何处使用动态效果时, 请使用 SwiftUI。 其内置的视图和控件可为您提供多种动态效果。 自动添加修饰符,以针对您的应用优化这些效果。
使用状态驱动的动画来响应事件, 并引导用户的注意力聚焦于重要内容。 通过精心挑选的一组自定义动画, 让您的应用脱颖而出。
感谢大家参与本次 SwiftUI 动态效果的导览。 希望这能帮助您了解使用 SwiftUI 能够实现哪些可能性。 现在,请欢迎 Leah 重返 Big Sur 舞台。
太棒了! SwiftUI 能通过 动态效果轻松为应用增添个性, 这真是太酷了。 我特别喜欢那个绿色弹跳脸的动画。 太棒了。 接下来,我们将休息片刻享用午餐, 请大家抽空品尝一下茶点。 可以提问,也可以结识新朋友。 下午的议程安排得很满, 我们将于太平洋时间 12:15 再次在此集合。
希望大家在休息期间过得愉快, 并有机会提问或结识新朋友, 下午我们将迎来内容丰富且充满乐趣的环节。 接下来,我的同事 Cole 将深入讲解 SwiftUI 数据流的细节。 请大家和我一起欢迎他登台。
大家下午好。 我叫 Cole, 是 Apple 公司核心技术布道师。 今天我将探讨应用中的数据, 以及如何将其流向 SwiftUI 视图。 “愿望清单”应用是一个绝佳的示例, 我将在今天全程使用它来演示 SwiftUI 应用中 可能遇到的各种数据流方式。
“愿望清单”应用在数据处理 方面做了许多与各位应用 可能需要类似的事情,并且会根据 具体数据类型采取不同的处理方式。
我将首先讲解其中一些最重要的内容。 在这个应用中, 某些地方的用户交互会导致 界面状态发生变化。 例如,当用户修改行程时, 应用需要跟踪 UI 是否处于编辑模式。
这种情况会在几个不同的场景中出现, 比如跟踪点击按钮后弹出表单的时机, 或者需要显示提示框的时刻。 我通常将这些情况统称为“视图状态”。
此外,该应用还包含一个数据模型。
“愿望清单”应用的目标 是展示我那些精彩行程和活动中的所有丰富数据。 例如,这次京都之旅就列出了 许多当地可以参与的有趣活动, 比如探索寺庙、 日出时分沿运河小径漫步等等。 因此,我需要在应用中对这些数据进行建模, 并将其流式传输到所有的 SwiftUI 视图中。
此外,应用还需要跟踪用户 在应用中设置的偏好选项。 例如,应用 在行程活动列表中提供了一个排序按钮。 你可以按标题、日期或活动是否已完成进行排序。 应用需要记录用户上次选择的排序方式, 以便下次显示该视图时沿用相同的排序方式。
最后,我想开发一个应用。 抱歉。我是想构建一种数据持久化方案。 数据持久化是一种让应用将数据 保存到设备存储(例如文件中)的方法。 这样,当用户下次返回应用时, 即使应用被重新启动,所有更改仍会保留。
因此,在本节中,我将详细讨论 上述每个用例详细讨论这些用例。 其中包括视图状态的数据。 构建丰富的数据模型、处理偏好设置和配置, 以及一些持久化技术。
本节结束时, 您将能够对这些用例进行分析, 并掌握可复用的模式, 以解决您自己应用中的类似需求。 首先,我将从视图状态开始讲起。
正如我之前 提到的,有时应用程序只需创建 一段数据来跟踪界面本身的状态。 这些数据通常是临时性的, 当视图消失时重置它们也是安全的。 例如,示例应用程序会跟踪 “行程”是否处于编辑模式。 如果用户离开该行程页面, 随后又返回, 那么界面不再处于编辑模式是合理的。 即使我没有点击“完成”按钮。
这里还有另一个需要在此应用“愿望清单 ”标签页中进行状态跟踪的示例。 工具栏中有一个“添加”按钮。 刚才 Curt 在优化动画 效果时曾提到过这个按钮。 点击此按钮会弹出一个弹出窗,用户可以 在其中开始输入新 行程的详细信息以添加到应用中。
因此,应用需要一个状态来跟踪 弹出窗当前是否显示在屏幕上。
此外,弹出窗本身还包含用于 保存更改或取消添加行程的按钮, 因此弹出窗中的这些操作 需要一种方式来更新状态, 以便 SwiftUI 能关闭该弹出窗。
以下是在 SwiftUI 中 实现该“添加”按钮的方法。 在 WishlistView 的主体中, 有一个带有符号标签的 SwiftUI 按钮, 我需要在按钮的动作中添加 一些代码来显示该弹出视图。
以呈现该弹出视图本身。 我需要使用添加到视图 上的 `isPresented` 修饰符。
现在,在完成这段代码之前, SwiftUI 需要我做出一个决定。 什么数据能追踪该 弹出视图当前是否实际显示在屏幕上?
这正是使用 `at` 状态 API的绝佳用例。其工作原理 如下:我将创建一个名为 `isPresentingAddTrip` 的新属性, 并使用 `at state` 对其进行装饰, 其默认值为 `false`。
使用 `at state` 会告诉 SwiftUI 创建一个与该 视图生命周期相同的新数据。 关于这个值的生命周期, 我稍后会详细说明,但现在,既然已经定义好了, 我就可以在视图的其余部分使用它了。 在按钮操作中,我会将 `isPresentingAddTrip` 设置为 `true`, 因为点击按钮应该总是会让弹出窗口出现, 而如果如果弹出表单已显示,则通过将该状态 传递给 disabled 修饰符来禁用按钮。
请注意,由于视图主体会像此修饰符中那样读取 isPresentingAddTrip 的 值, 因此 WishlistView 会对此值建立依赖关系。 每当 isPresentingAddTrip 发生 变化时, SwiftUI 都会更新此视图。
我还将把 isPresentingAddTrip 属性传递给弹出表单修饰符。 这种美元符号表示法允许我 将一个 Binding 传递给该状态, 使这段代码能够与弹出窗口共享该值, 从而既能读取该值, 也能对其进行修改。 稍后我会回来详细解释绑定。
用 at state 修饰一个属性,会告诉 SwiftUI 该属性由外围视图拥有。
只要视图存在,该属性的值就存在,
这是我刚才提到的一个重要点。 在界面中,带 `at` 状态的属 性与视图的生命周期一致, 而非视图结构体本身的生命周期。 因此,我将进一步探讨其实际工作原理。
正如 Leah 今天早些时候所讨论的, SwiftUI 视图只是短暂 存在的描述,就像一个模板。 当 WishlistView 在界面中出现时, SwiftUI 会运行其 主体以更新屏幕上的 UI, 但随后会丢弃该实例,
并且每次 SwiftUI 渲染 视图时都会重复这一过程。
那么,既然默认值为 false, 为什么 isPresentingAddTrip 属性在该视图被重新初始化时不会被重置呢?
这是因为 SwiftUI 会对像这样处于 “状态”中的属性给予特殊处理。
在幕后, SwiftUI 拥有自己的数据存储空间, 用于存放所有视图拥有的状态。 当 SwiftUI 首次实例化该视图时, 它会在内部存储中为该属性分配空间, 并将其与其他视图拥有的所有状态一同存放。
该存储空间通过视图的标识进行索引。
可以将标识理解为对应于该视图 在屏幕上渲染结果的生命周期。 因此,当 SwiftUI 首次渲染该视图时, 会为其分配一个标识。 当用户离开该视图时, SwiftUI 会从其存储中删除该条目。 如果用户稍后返回该视图, SwiftUI 会分配一个新的标识符, 并在存储中分配一个新的条目。
下面将演示这一机制如何应用 于我的按钮:当用户首次导航 至 WishlistView 并点击 该按钮时,它会显示一个弹出片。 SwiftUI 会为该 视图的 isPresentingAddTrip 状态属性分配存储空间, 并使用默认值 False。
随后,它实例化该结构体, 并将 `isPresentingAddTrip` 的值设置为与 SwiftUI 的内部 存储一致,接着执行 `WishlistView` 的主体代码以 在屏幕上渲染内容,当用户点击该按钮时,
SwiftUI 便会丢弃该视图实例。 该操作闭包会将 `isPresentingAddTrip` 设置为 `true`, 但由于这是状态属性, 此赋值操作会修改 SwiftUI 的内部存储; 且由于视图的主体依赖于该状态, SwiftUI 会触发更新:在运行主体之前, 会创建一个新的视图结构体, 并从内部存储中获取新的 `true` 值来填充 `isPresentingAddTrip` 的值。
因此,正是通过这种内部存储, SwiftUI 才能 在界面中维护视图生命周期内的状态。 尽管这些结构体本身是暂存的。
好的,这个内部存储解释了“ at state”属性如何让 SwiftUI 在界面中跟踪该视图生命周期内的布尔值。 但“at state” 不仅在单个视图声明中有用。 它还可以通过一种称为“绑定”( Binding)的机制与其他视图共享。
我在弹出窗口修饰符中 使用的这个美元符号,允许我访问指向 `isPresentingAddTrip` 状态的 绑定。
绑定只是对应用中某个状态的读写引用。
当你创建一个需要从其外层视图读取值、 并且可能需要修改该值的视图时,绑定非常有用。
绑定在 SwiftUI 中随处可见。 你会在切换按钮、文本字段以及 Sheet 修饰符等的参数中看到它们。 这些是封装好的控件, 它们依赖于这些数据,并在需要时 可以修改这些数据。 例如,以下是“添加行程”视图的代码片段, 该视图位于弹出表单内部。
它使用一个绑定,从父视图接收对状态的引用, 该状态用于跟踪弹出表单是否在视图主体中显示。 其中有几个按钮可以导致弹出表单被关闭。 为了实现这一点,它们在动作 闭包中将 `isPresented` 绑定的值设置为 `false`。 这会促使 SwiftUI 更新 所有依赖该状态的视图, 比如我的视图。 关闭视图后,弹出表单便会被收起。
像我的弹出表单这样,仅需跟踪 用户与界面交互时机的场景, 非常适合使用 atState。 atState 是一种临时数据, 当视图消失时应被重置, 或者适用于那些仅在视图 存在期间才应存在的对象。
但“At state”并不适合 其他类型的数据,比如我的数据模型。 对于这些情况,你需要一种方式向 SwiftUI 表达由代码中其他部分拥有( 而非由 UI 本身拥有)的状态类型。
这便引出了我的下一个用例, 即“愿望清单”中的数据模型。 数据模型是这款应用的核心与灵魂。 它包含了用户愿意反复使用该应用的所有信息, 例如每次旅行的名称、照片和活动。 就像这个示例 中一样,旅行对象名为“秘鲁越野之旅”, 并关联了一张绝美的照片。
这些对象之间还存在关联关系。 例如,一次旅行可以包含多个活动, 而活动本身也有自己的属性, 如名称和一个布尔值, 用于标记该活动是否已完成。
因此,我使用类创建了一个数据模型, 用来表示该应用中的旅行和活动。
每个类都有属性来存储名称、照片和日期等信息。
它们还存储了彼此的引用, 以便建模这些对象之间的关系。 例如,旅行对象可以通过 在 activities 属性中存储对相关活动的引用, 与任意数量的活动建立关联。
这些对象也是可编辑的, 因为用户可能会在用户界面中修改这些属性。 比如当用户在文本框中编辑旅行的名称时。
我还创建了一个名 为 DataSource 的类, 用于管理应用中的所有这些对象。 这是一个让应用能够查询所有需要在 UI 中显示的对象的统一位置, 例如获取所有行程的列表。
因此,拥有这样一个数据模型是一个很好的开始。 但要想在 SwiftUI 中 真正使用这个数据模型, 我确实需要添加一个简单的步骤。
数据模型需要能够感知其属性发生变化的时刻, 这样 SwiftUI 才能更新 UI。
而这就是 `Observable` 宏的作用。 仅需这一行代码, 你就能让 SwiftUI 能够针对类中的属性 建立依赖关系。
Observable 宏为类的每个 属性添加了更新跟踪功能。 要使用它,只需用 @Observable 宏修饰每个类即可。
现在数据模型已成为 Observable, SwiftUI 便可以直接 与模型属性建立依赖关系。
在此示例中, TripCard 视图 获取了应显示的 Trip 对象的引用。
请注意,这里没有 @State 或 @Binding。 事实上,该引用上根本没有任何装饰。 使该视图正常工作的关键 在于:在视图主体内部,它读取了 `Trip` 的 `photoURL` 属性。 这告诉 SwiftUI, 每当该 `Observable` 类上的 `photoURL` 属性发生变化时,
`TripCard` 就需要更新。 这同样适用于计算属性。例如,假设我 想为尚未保存照片的行程使用占位图。 UI 中处理此情况,我可以 在 `Trip` 上定义一个名为 `photoURL` 或 `placeholder` 的计算属性。 如果该行程未定义照片, 该计算属性将返回一张占位图。 然后,我在视图主体中使用这个新的计算属性, 而不是 `photoURL`。
当访问该计算属性时, SwiftUI 能够确定 视图主体中底层属性 `photoURL` 已被读取。 因此,每当底层照片 URL 发生变化时, SwiftUI 都会 更新 `TripCard`。
好的,总结一下,这个应用使用 `Trip` 和 `Activity` 对象构成的结构化数据模型。
它使用引用类型或类来建模每个对象, 这样我就可以通过引用来建模 这些对象之间的关系。 而且,这些属性在UI的任何 位置都很容易进行编辑。
为了让这些类中的数据流向 SwiftUI, 我只需在每个类的定义中添加 `at Observable` 宏。 这样,我的 SwiftUI 视图就 可以直接在视图主体中读取这些属性了。
现在还有一件事需要做。 到目前为止,我演示的内容运行得非常顺利。 一旦视图已经拥有了对某个对象 (例如 `Trip`)的引用。
刚才我提到,所有这些对象都由 `DataSource` 类拥有,但如何告诉视图 应该使用哪个数据源实例呢?
其实,我之前已经讨论过实现这一点的一种方法。 它始于 `at state`。
回想一下,在 at state 中, 我们会创建一个与视图 生命周期相同的对象。因此,在应用程序的顶层, 我可以创建一个名为 at state 的属 性来保存数据源对象。 将 at state 放入此应用 程序声明中,会创建一个与应用 程序生命周期相同的对象, 然后将其传递给各个视图。
对于本应用中非常简单的视图层次结构, 这种方法可能效果很好。 但用户界面的不同部分可能需要访问数据源。 例如,要获取完整的行程列表, 且视图层次结构非常简单时, 直接将类似 `DataSource` 的对象 传递给需要它的视图, 这种做法完全合理。
但随着应用复杂度的增加, 这种方法可能会变得笨拙。 一个具有多层嵌套结构的应用, 最终可能需要传递并存储数据源的引用, 仅仅是为了将其传递给更深层、需要它的视图。
因此,为避免这个问题, 此类场景非常适合使用环境( environment)。
可以将环境视为配置应用 各部分的核心属性, 且这些属性通常不会频繁更改。
在此情况下,我将创建一个数据源对象作为状态, 并将其设置到视图的环境中。 这会将需要数据源的视图 配置为使用该特定实例。
这种做法是安全的, 因为数据源对象本身不会频繁被替换。 它具有可观察性( Observable), 而且我的应用中有很多 不同的视图可能需要引用它。 因此,通过将数据源放入环境中, 任何需要引用它的视图都可以直接获取。 环境会流经其所应用的整个视图层次结构。
视图只需从环境中请求一个值, SwiftUI 就会提供 该值。 例如,在视图层次结构的更深处, RecentTripsPageView 通过为一个属性添加环境 装饰来获取对数据源的引用。 SwiftUI 会将该引用 填充为来自环境中的匹配类型的对象。
由于 `DataSource` 是可观察的(Observable), 视图便可直接在视图主体 内对其属性建立依赖关系。 在此处,该视图主体中读取了最近 添加的 `Trips` 属性, 因此每当应用中新增一条行程时, 最近行程页面视图都会自动更新。
到目前为止,视图状态和我的数据模型已经 涵盖了该应用所需的大部分数据流。 但还有其他几种用例需要采取略有不同的方法。 因此,我将继续探讨该 应用中的偏好设置和配置。
我之前提到过,当用户点击某条“行程”时, 可以按名称或是否已完成对显示的活动 列表进行排序。
应用需要存储这一偏好设置, 以便用户每次进入该视图时, 都能采用其首选的排序方式。 不过,将偏好设置 放入我之前讨论的数据模型中并不太合适, 因为这实际上与行程本身没有任何关系。 这属于用户偏好层面的内容, 反映了用户在使用应用不同功能时的不同倾向。
因此,要实现这 一排序功能,我可以先定义一个新的状态, 用于记录当前使用的排序方式。
在这个示例中,它被称 为 sortOption。 随后, Activity 子视图在布局其 活动时会读取 sortOption。
目前这套方案可行,活动列表会根据用户在菜单 中的选择进行排序。
但请注意,像这样的 at state 属性仅 在视图生命周期内存储。 因此,如果用户离开该视图后再次返回, 排序方式将始终恢复为默认值,即按标题排序。 我希望实现这样的效果:无论 用户何时返回“行程详情”视图, 其排序方式都能保持与之前选择的一致。
为此,我将把 at state 改 为 at AppStorage, 并为该设置提供一个唯一标识符。
at AppStorage 的工作 原理与 at state 非常相似。 它声明了一段在整个应用中全局有效的状态, 且最适合用于简单的 Codable 类型。
AppStorage 的值 实际上会自动存储到磁盘上。 在底层,它使用 Apple 平台 上的一个名为“用户默认值”( User Defaults)的 API, 该 API 会为你的应用存储一组键值对。 这是存储设置、 偏好和应用配置细节的绝佳位置。
因此,通过将排序偏好保存 到 AppStorage 属性中, 现在无论用户在多个行程之间如何切换, 活动都会始终按照用户在视图 中最后选择的选项进行排序。
最后,今天我想讨论的最后一个用例是持久化。
刚才我演示的那个使用 AppStorage 保存排序偏好的示例, 实际上就是持久化的一个实例。 SwiftUI 会自动调用 User Defaults API 从磁盘获取该键的值, 然后将视图中的值设置为与之匹配。
如果为 sortOption 赋予了不同的值, AppStorage 会将该新值 保存回 User Defaults。 因此,即使用户在一天后、 一周后或一个月后重新打开你的应用, sortOption 属性也会保持不变, 直到被修改为止。
当然,你可能希望持久化的不仅仅是偏好设置。 这款应用拥有丰富的数据模型, 用户肯定希望将他们的“愿望清单”保存数天、 数周甚至更长时间。
因此,若要在应用中构建此类 具备持久化功能的数据模型, SwiftData 是一个绝佳的选择。
SwiftData 是一个框架, 它能让你快速为应用添加持久化功能, 代码量极少且无需外部依赖。
它利用了宏和属性包装器等 现代 Swift 语言特性, 让你只需编写 Swift 代码即可描述模型。
默认情况下,它利用了 Core Data 久经考验的持久化功能。 SwiftData 中的技术 和模型均自动支持可观察性。
要进一步了解 SwiftData, 请观看相关视频,了解 SwiftData, 并使用 SwiftData 构建应用。
正如我之前所分享的,数据模型由几个类组成, 其各个属性 以及类之间的关系都作为属性存储 在这些类中。 目前,这些类都使用了 Observable 宏进行装饰。
但如果我想使用 SwiftData 实现它们的持久化, 只需将 Observable 宏 替换为 model 宏即可。
model 宏会将这些类转换 为 SwiftData 模型。 同样,这也会自动使它们成 为 Observable。
作为一项有趣的练习, 你可以尝试在示例代码发布后下载它, 并按照几个步骤将该应用迁移 到 SwiftData 上。 对于有兴趣尝试的朋友, 我将讲解代码中需要修改的几个关键点。 首先,你需要为模型添加一些元数据。 你需要优化从应用视图 内部获取或查询数据的方式, 并设置一个容器来存储你的模型。 下面我将快速概述这些步骤。
首先,将模型从 `at Observable` 切换 为 `at model` 之后, 你需要遍历各个模型,并对希望向 SwiftData 提供 更多信息的属性添加注解。 例如,在 `Trip` 模型中, 你可以在 `activities` 属性上使用 `at` 关系来定义 行程与活动之间的关联。 这里我将删除规则设置为“级联”, 这样当一个行程被删除时, 其所有活动也会被删除。
我还为 `trip` 属性设置了反向关系, 即 `trip` 属性 在 `Activity` 上的反向关系。 这意味着这些 `Activity` 上的 `trip` 属性 将始终指向其所属的 `Trip`。
现在,借助 `at Query`, SwiftData 让在视图中 获取和显示模型变得极其简单。 你只需向 SwiftData 请求一个按你所 需方式排序和过滤的模型数组即可。
在这个代码片段中,我更新了之前 展示的 `RecentTripsPageView`。 以前,它使用数据源对象来获取 最近添加的行程列表,但借助 SwiftData, 我现在只需 使用 `at` 查询即可。
该查询要求 SwiftData 提供一个按 创建日期降序排序的行程数组。
随后,视图主体通过 ForEach 循环使用这个最近添加的行程数组。
如果查询结果发生变化(例如 向视图中添加了新的行程), SwiftData 会自动更新该视图。
最后,在应用声明中, 您需要通过添加 modelContainer 修饰符并传入模型类型的名称, 配置一个用于存储模型的容器。 应用会为其模型使用默认容器,
因此这三项 更改:添加属性元数据、 优化查询以及添加模型容器, 是入门的三个关键步骤。 这为像这样的应用带来了 SwiftData 强大的持久化能力。
至此,我已详细讲解了“愿望 清单”应用的所有数据流用例, 您也可以在自己的 SwiftUI 应用中采用类似的方法。 当应用中的视图只需跟踪简单的 UI 状态(例如按钮被点击时), 请使用 `atState`, 然后通过绑定让其他视图或控件共享该状态。
对于使用类等引用类型 构建的复杂数据模型,建议考虑 使用 `Observable` 宏。
当需要保存数据时, 请将数据保存到存储中,以确保其持久存在。 对于设置和偏好等小数据, 请使用 AppStorage。 对于模型数据,请考虑使用 SwiftData 框架。 感谢大家的聆听。 希望大家享受接下来的活动。 现在将话筒交还给 Leah。 好的。
刚才的分享太棒了。花时间学习数据流的基础 知识以及何时使用每种工具,这一点非常重要。
接下来,我们有一位特别嘉宾— — AllTrails 的首席 技术官 James Graham, 他将分享他们从一个复杂且成熟的 UIKit 应用迁移到 SwiftUI 的经验。 请和我一起欢迎 James 上台。
大家早上好,或者下午好。 我有一个问题想问大家。 也许大家能感同身受。 你们是否曾审视过自己的旧版 UIKit 代码库? 也许看到四年前编写的一个庞大 视图控制器时,会心想:“天哪, 这得好好重构一下了。 ”“干脆在上面插个图钉, 把整个项目重写一遍吧。 ”现在请大家举手示意,有多少人用过 SwiftUI?你们是否遇到过类似的情况? 哇,真多。我看到有非常非常多的人举手了。 我相信还有更多在线观看的朋友。 这个想法虽然诱人, 但在我们这样的规模下进行重写—— 无论是依靠工程师的努力 还是 AI 原生工作流—— 都会带来巨大的风险。 大家好,我是詹姆斯 · 格雷厄姆。 我是 AllTrails 的 CTO, 今天我想和大家聊聊我们如何在不 重写代码的情况下实现现代开发速度。 我想向大家展示,我们是如何让 SwiftUI 自然融入我们的系统, 而不是自上而下地强制推行。 在深入探讨之前,先来概述一下本次分享的框架。 首先,我将介绍 AllTrails 是什么, 我们的规模以及面临的限制。 然后,我将讲述 SwiftUI 如何 在无需重写代码或强制要求的情况 下融入我们的代码库。 之后,我将向大家展示那个转折点。 那时, SwiftUI 已 不再是实验性的尝试, 而是逐渐成为了默认选择。
最后,我将总结当前的状况, 并阐述我们对混合架构的看法。
要理解我们的技术决策,必须先了解我们的规模。 如今, AllTrails 是全球最受欢迎、 最值得信赖的户外探索平台。 我们的使命很简单:帮助全 世界的人在户外找到自己的路。 我们帮助人们发现步道、 自信地导航,并提升他们在步道上的体验。 通过提供最新的步道详情 以及“照片导览”等功能, 突出沿途的精彩照片。 无论是当地公园的散步, 还是为期数天的徒步旅行, 我们都能满足您的需求。 AllTrails 拥有超过 9000 万名社区成员。 我们收录了全球 50 万条步道, 会员累计行走里程已超过 19 亿英里。
我们的服务支持 14 种语言, 这意味着我们做出的每一项技术决策, 都会影响数百万使用不同设备、身处不同地区、 且关键的是处于不同网络连接水平的会员。
我们服务于兴趣和偏好各异的广泛会员群体。 一方面,有希望在当地湖畔享受美景、 进行轻松且相对平坦的午后 散步的休闲用户;另一方面, 也有正在挑战全天半穹顶徒步路线、 在无手机信号的情况下 依靠离线导航的狂热徒步者。
这种多样性对可靠性、 电池续航和 UI 性能提出了严格的限制。 我们不能发布存在缺陷的代码, 但我们的应用并非一成不变。 它随着新界面的出现和新功能的深入而不断演进。
当 SwiftUI 问世时,它带来了希望。 当时 AllTrails 已经是一款规模庞大、 成熟且成功的 UIKit 应用。 作为这种演进的一个例子, 以下是我们主页历年的变化。
我们采用每周发布周期,同时支持免费和付费 两种使用体验。
关于我们的遗留代码,我要强调最重要的一点是 : UIKit 并不是一个需要修复的问题。 它是支撑我们规模扩展的基础。 正因如此,我们无法暂停服务来进行改动。 我们必须维护它,也必须对其进行升级。 就像在徒步途中一样。
当 SwiftUI 问世时, 它带来了我们迫切渴望的功能。 更简洁的状态管理, 视图会在数据变化时自动更新。 消除了 UI 与模型不同步这一整类 bug, 且代码量更少。 与 Uikit 的等效实现相比, 代码量减少了 40%。 这意味着需要维护、 更新和阅读的代码减少了 40%。
实时预览功能让我们能够即时迭代设计变更, 但从技术和组织层面来看, 重写一个成熟的应用程序并非可行之选。 我们需要一种不同的方法。 于是, SwiftUI 悄然 融入了我们的代码库。 这既不是强制要求,也不是路线图中的项目。 我们创建了 一个沙盒环境,将其用于原型和独立 服务的低风险实验。 这为我们提供了一个学习该框架的空间, 同时无需将我们的候选发布版本 押注于此。
最初的真正决策并非在于选择 UIKit 还是 SwiftUI。 而是如何让它们协同发展? 我们很早就开始投入互操作性的开发。 请看这段代码片段。这就是我们的桥梁。
我们选取 SwiftUI 的一项功能, 比如我们的“trail coordinator ”。 将其封装在 HostingView 中,然后直接放入标准的 UKit StackView 中。
接着将其添加到 ScrollView 中。
当我们向下滚动页面时, 你可以看到 HostingView 的某些 部分包含 SwiftUI 子视图。 我们很早就确立了这种模式。
这是一个流程 层面的决策,旨在确保边界清晰, 并让两个世界能够“同频沟通”。
随着时间的推移,两条平行轨道自然形成。 UIKit 仍然为我们承担着繁重的工作。 例如应用生命周期的导航, 以及像我们的“路线”页面或社区“ 活动”页面那样复杂或深度集成的界面。 在成熟应用中,这些视图已相当成熟。 仅仅为了更换框架而重写 那些稳定且经过实战检验的界面毫无意义, 因此我们避免在徒步途中 重新规划一条标记清晰的路线, 而是专注 SwiftUI 方面能为用户带来 明显体验提升和开发效率提升的领域。 我们有意将 SwiftUI 应用于 那些孤立且边界明确的界面中—— 无论是渲染资源密集型视图还是进行新的实验。
您可以在这里看到两个相关示例。 “Trail Review” 流程是一个具有动态状态和 UI 更新的自包含界面,这 与 SwiftUI 的声明式模型高度契合。
基于 Apple 智能构建的“Ask the Trail Anything ” 功能则是一项更新、更具实验性的服务。 SwiftUI 让我们能够 在此快速迭代并优化用户体验, 同时避免与应用的核心架构产生紧密耦合。
SwiftUI 在我们的设计 系统中同样大放异彩。 让我们在调试模式 下看看我们的应用,它可视化了名 为 Denali 的设计系统。 Denali 每天都在不断扩展, 我们已联合设计、 系统和工程团队, 确保所有新功能都能利用该系统。
你们在这里看到的所有核心组件— —按钮、分段控件、控件、徽章— —都是我们设计系统的一部分, 可在应用的调试模式下查看, 现在均由 SwiftUI 构建而成。
过去需要数百行模板代码才能实现的功能, 现在只需其中的一小部分代码即可完成。 而且当我们需要添加新变体或调整间距时, 只需进行一次简单的修改, 更改就会自动同步到所有相关位置。 SwiftUI 让我们能够在不 增加代码量的情况下扩展设计系统。
在采用 SwiftUI 时, 我们还注意到一个意想不到的变化。 它开始影响我们的架构。
视图模型变得更小了,通常缩减了三分之一, 因为我们不再需要编写仅用于保持 UI 与状态同步的粘合代码。
此外,由于 SwiftUI 替我们处理了状态传播, 我们编写的显式基础代码、发布器、 运算符以及生命周期管理代码也大幅减少。
而且,由于 UI 状态和行为紧密结合, 更改所涉及的代码行数更少。 我们的 UI 拉取请求体积 缩小了 30% 至 40%, 代码审查速度也明显加快。
正是从那时起, SwiftUI 不再 让人感觉像是一次 UI 实验, 而是开始被视为构建系统的正确方式。 我们从未强迫工程师使用 SwiftUI。 他们选择在新的项目中采用它, 是因为它减少了开发阻力。 它降低了认知负担。
当选择正确时,代码量减少 40%的采用率便会形成良性循环。
这就是我们得到的启示。
SwiftUI 的普及并非 因为我们要求大家使用它。 它之所以普及,是因为它是前进的最快路径。 当一个框架能降低认知负担并消除复杂性时, 工程师无需被说服。 我们自然会选择它。
但代码库呢?它并非一夕 之间就发生了翻天覆地的变化。 它只是逐渐倾斜。UIKit 依然根深蒂固。 SwiftUI 正围绕着它发展。 我们通过诸如“每项功能所 需代码行数减少”、“迭代
周期加快”以及“ 孤立的 SwiftUI 功能中回归 问题减少”等指标来衡量成功。
我们意识到,互操作性就是基础设施。 我们投入资源开发了宿主封装器、共享动画、 桥接组件以及统一的主题系统。 当桥接机制稳固后, SwiftUI 就不再让人觉得是新事物, 而是成为了基础架构。
我们的 Apple Watch 应用就是这方面的完美例证。 无论是“指南针”还是“地图”界面, 都是用 SwiftUI 构建的, 而我们的地图功能则使用了 MapKit。 这展示了我们如何利用现代 架构交付关键的、对性能敏感的功能。
我们在此选择 SwiftUI 并非因为它“很酷”, 而是因为它让我们能够 在不触及旧版导航逻辑的情况下, 更快地迭代复杂的界面。
那么,我们目前处于什么阶段? AllTrails 尚未完全 迁移到 SwiftUI。 您无需重写整个应用就能获得 这些好处。我们是一个有 明确发展方向的混合系统。 UIKit 提供了稳定性。
对于深度 UI 定制(如复杂的集合视图或精细的导航栏), 它依然表现出色。
SwiftUI 则定义了我们的发展方向。 它正在迅速迎头赶上, 如图所示的“植物识别”功能就是 100% 基于 SwiftUI 实现的。
因此,我迄今为止所描述的一切都 与 AllTrails 自身的情况密切相关—— 我们的规模、用户群体以及面临的限制。 而这正是我们有意为之的。
团队常犯的一个错误,是将采用 SwiftUI 视
为一个非此即彼的决定。 “我们应该采用 SwiftUI 吗?
”更好的思考方式是:“在什么条件下, 采用 SwiftUI 能带来发展势头而非风险? ”当我们审视真正对我们 有效的因素时——更少的 bug、 更快的交付、更好的跨团队 扩展性——这并非单一的技术选择。 而是一系列经过深思熟虑的决策。
我经常被问到一个问题: UIKit 和 SwiftUI 能否安全共存? 从我今天展示的内容来看,答案无疑是肯定的。 因此,与其告诉大家该采用什么, 我不如提出三个问题, 供大家根据自身情况评估采用决策。 第一,互操作性是否被视为基础设施? 如果互操作性脆弱或缺乏规划, 一旦面临实际的产品压力,采用进程就会停滞。
第二,新工具能否降低认知负担? 当一个工具真正简化了思维负荷时, 采用就不需要强制推行。 工程师会自发地选择它。
第三,你关注的是发展势头,还是代码转换率? 发展势头体现在更小的 PR、 更快的代码审查以及更少的回归问题上。 而不是体现在你转换了多少代码库。
因此,如果这里有什么值得 借鉴的经验,那就是:你无 需重写代码也能取得进展。 你需要的是方向。非常感谢大家今天抽出时间, 也感谢让我分享 AllTrails 的故事。 我们在山径上见。
非常感谢你,詹姆斯。 能亲耳听到 SwiftUI 如何让 功能发布更轻松、减少维护工作, 以及它能与 UIKit 代码库如此顺畅地共存,这真的很鼓舞人心。 我是 AllTrails 的忠实粉丝, 能深入了解其幕后开发过程让我倍感愉悦。
好的,我们再休息一次,随后请 于太平洋时间 22:25 回到这里, 参加由 SwiftUI 工程 领域领军人物带来的专题讨论会。 到时候见。 希望大家休息得愉快。 现在,是时候迎来由三位 SwiftUI 工程领域领军人物参与的小组讨论环节了。
请大
家和我一起欢迎 Nick、 Russell 和 Taylor 登台。 说得好, Leah。 虽然我之前有幸与各位共事过,但为了让现场观众更了解你们,请大家先做个自我介绍,并 分享一些个人经历。你们在 Apple 工作多久了?哦,我叫 Nick Tyler。 我在 Apple 工作了大约六年。 我最初加入了 MapKit 团队, 这本身就已经是一个梦想成真, 因为我刚了解到 MapKit 工程师们 如何开创了面向对象编程的先河, 而且至今仍有机会与当年在 Next 和 Apple 工作的前 辈们共事。 所以,这就是我加入团队的经过。 太酷了。 我是拉塞尔,现在是 SwiftUI 经理, 但我已经在苹果工作了九年,直到最近之前, 我在这里的大部分职业 生涯主要都是作为 UIKit 工程师。 而且,我是大学毕业后直接加入苹果的。 我是泰勒,我想我应该是这里年纪最大的, 毕竟比大家大 13 岁。 我和尼克一样,最初也是从 MapKit 起步的, 后来转到了 SwiftUI, 而最近其实又回到了原点,重新承担起部分 MapKit 和 Catalyst 的工作。 所以能来到这里,我真的很兴奋。 我觉得,仅凭你们的经历和经验, 大家就已经在讲述 MapKit、 UIKit 和 SwiftUI 之间的互操作性故事了。 所以,请再多谈谈是什么促使你们 转型并投身 SwiftUI 开发, 以及你们喜欢这个框架的哪些方面。 嗯,在与 AppKit 团队合作—— 他们是面向对象设计的先驱——之后, 我看到泰勒转到了另一个团队, 在那里开创了声明式框架设计。 所以我当时真的觉得,这才是未来的发展方向。 是的。此外,我想,声明式设计—— 这种声明式的本质——确实也是吸引我的地方。 尤其是它由 Swift 驱动这一点。 比如你可以编写一个声明式系统— —可能需要补充说明一下, 当我们说“声明式”时, 是指你告诉框架你希望发生什么, 而不是列出所有实现步骤, 后者就是命令式编程的方式。 因此,这是一个非常强大的理念:你可以用 任何语言构建声明式系统, 包括 Objective-C。 但 Swift 让这种编写方式 显得更加自然且富有表现力。 因此,这确实是一个真正 探索这些理念的独特机会。 这也是吸引我的地方。 是的。 哦,抱歉。我本不想打断你。 哦,我也觉得这就像是 在尝试解决我一直以来都感兴趣的问题, 那就是让在 UI 中开发应用变得更轻松。 各种框架之间其实并没有那么大的区别。 而且 SwiftUI 也 并非不具备命令式特性。 比如当你深入到布局协议、 画布或路径时,你会发现, 拥有声明式 API 其实是另一层有用的抽象, 有助于解决我们一直 试图解决的那类问题。 拉塞尔有一个他经常提起的梦想, 那就是一个没有bug的未来,在那样的未来里, 应用会完全按照你的意愿运行,一切都完美无缺。 是啊,我也想生活在那个现实中。 你有途径进入那个世界吗?能帮我安排一下吗? 我觉得就是这样。 我的意思是,我们已经有了标志和其他一切。 这真的很了不起。我特别喜欢你 描述的 SwiftUI 所追求的简约性— —在我看来,从我第一次 使用这个框架的那一刻起,它就实现了这种简约。 这种声明式界面不仅让开发 过程优雅流畅,更让人乐在其中, 愿意投入时间去学习。
所以 SwiftUI 是 2019 年推出的。 你能详细说说它原本打算解决什么问题吗? 还有,这个问题陈述和解决 方法随着时间的推移是如何演变的。
我先来说。 泰勒。 你资历更深,对吧? 是的。 大多数年份里,我的意思是, 我认为,你知道,我试图解决的问题 主要是降低构建应用的门槛,对吧。 并充分利用声明式系统等特性。 你知道,我们曾描述过, 我们真的希望有一个单一的“ 真相来源”, 这样你就不会遇到在其他类型的应用 中可能出现的那些错误。 所以这些确实是我们的目标。 它们是我们所做工作的核心。 因此,最初的几年主要是为这些原则打下基础, 以便在此基础上进行构建。 而之后的每一年,重点都在于增加新的功能。 要知道, UIKit 和 AppKit 提供的功能已有悠久的历史。 我们的目标就是与之匹敌。 时至今日,我们虽然尚未完全 达到这一目标,但我认为 我们正逐渐接近一个临界点—— 人们能够构建出的应用类型已让我印象深刻。 补充一下泰勒刚才的话, 我认为从一开始就始终如一的一点是:我们 在设计 API 时秉持“学一次, 写遍处”这一理念。 但还有这样一个理念: 一旦你熟悉了框架的某些部分, 你自然也会熟悉API的许多其他部分。 试想一下 UIKit : 当你学习了 UITableView 并 掌握了其 API 接口后, 转而使用另一个组件时, 可能会觉得“哦,这个组件有不同的模式, 我需要重新熟悉”。 但在 SwiftUI 中, 其设计理念是,从一开始, 一切都让人感觉 很熟悉。 因此,这一直是…… 一直是一股稳定的、稳定的驱动力。是的。 而且我认为这不仅体现在实际 使用 API 上,还体现 在能够将一段代码 移植到不同的平台和硬件上, 从而在不同的场景中观察应用的表现, 并提升自己的技能。
我认为罗素在另一场讨论中提到的一点是, 随着我们逐渐接近实现应用所期望的完整功能集, 接下来将进入一个新的层面。 而且我认为,我们正在转向探索:究竟能用 SwiftUI 和相关工具 构建出哪些真正独特的东西? 是的。因为,我的意思是, 我觉得当初我们起步时, 我们确实已经奠定了一个良好的基础。 比如,当初这个构想非常扎实, 但随后却花了许多年时间仅仅是为了 与其他框架保持功能对等。现在,随着 我们逐渐接近这一目标, 我们可以将优先级重新转向核心领域—— 比如,我们能为框架引入哪些新思路, 以及如何推动核心框架本身的发展? 是的,完全同意。说到这个,我首先想到的是 新引入的 ScrollView API, 以及它们如何……虽然你比我 更了解这些细节,但它们确实 将功能提升到了一个新高度, 让开发者能够更轻松地实现非常复杂的视图, 而无需编写太多代码。 没错。 你提到了这一点,我非常高兴。 给听众提个快速问题。 在你的应用中, SwiftUI 的使用比例有多大? 你的 ScrollView 里是否 包含 GeometryReader? 看,有举手了。请举手。很好。好的。 我看到有几位举手了。 你们可能已经不需要那样做了。 还有其他一些已经存在两年甚至更久的 API, 它们正是专门设计来让这种操作 更加符合人体工学且切实可行的。 你们也是如此。 这里要谈的是滚动位置修饰符。 滚动位置修饰符。 有一些非常巧妙的方法,可以将你的动画 与 ScrollView 绑定并由其驱动, 我们也有专门针对此内容的专题讲座。 过去,你可能得追溯到 2023 年。 不过,不管怎样,我们。 把它扔掉吧。看一个单行滚动页面。 页面行。 Curt 演示了滚动 效果,滚动。目标行为,滚动,目标布局。 就连布局协议本身, 也在解决 GeometryReader 所需的一些需求。 所以我认为这些都是很好的例子, 同时也展示了反馈循环。 比如当我们看到人们如何 使用这些功能并遇到问题时, 这让我们能够思考下一步如何改进框架, 从而让用户 能够实现一些非常酷的功能,比如…… 我目前还无法准确解释, 但你可以使用坐标空间, 并通过——我记得是——滚动代理来访问 ScrollView 的坐标空间。 所以,利用滚动读取 器代理,你可以做 很多有趣的事情,而无需经历 GeometryReader 有时 会带来的无效化循环。 我觉得关于 ScrollView 的讨论可以没完没了。 你知道,可聊的内容很多, 但我想回到你之前 提到的一点:开发者带来的影响, 以及观察人们如何使用这个框架, 并希望让 SwiftUI 不仅能与 UIKit 和 MapKit 功能对等, 更能迈向更高层次, 让开发者更轻松地打造令人愉悦的用户体验。 你能谈谈开发者社区对 SwiftUI 的影响吗?
这正是最初的初衷。 当我对 Taylor 说“我 想加入你的团队”时, 他问:“你喜欢什么?”我回答说:“对我来说, UI 和 SwiftUI 并不代表用户界面。 ”它代表的是你和我。就是这样。 但这确实是真的。 就像,当你遇到任何人时, 如果你们有共同的爱好——比如你们 都是跑步爱好者,或者都喜欢某种类型的电影。 比如我在世界各地旅行时,曾结识了许多人, 仅仅因为我们都喜欢 UI 框架, 我主动搭话后, 我们立刻就有了共同的语言。 所以说实话,这真是一种探索世界、 结识新朋友的绝佳方式。
是的。我的意思是,就像我之前 描述的那种反馈循环: 每年我们都会发布我们认为接下来最重要的功能, 观察人们在构建什么、 他们如何回应,然后在此基础上进行迭代。 就像我之前说的, 最初的目标是降低应用开发的门槛。 尽管这是我们的目标, 但当 SwiftUI 发布后,看到一些从未开发过 应用的人开始首次尝试开发, 我们还是感到既惊喜又惊讶。 你知道,设计师就是其中一类人, 还有许多其他群体也是如此。 你知道,生成式 AI 工具 是另一个很好的例子, 它们真正赋能了人们—— 你知道,我每天都会看到这样的情况:有人说“我 开发了这款新应用,真的做到了我想做的事”。 而且这还是我第一次开发应用。 看到这些真的很酷, 然后我们还能观察 到,这些新入行的开发者会遇到哪些问题, 以及我们如何让这种体验变得更好? 我想补充的是, 我每天早晨起床的全部动力, 就是为了添加新功能, 让你们都能开发出精彩的应用。 毕竟,在我来这里工作之前, 我自己也曾是开发者社区的一员。 我当初就是通过阅读 Apple 关于 如何开发 iOS 应用的文档来学习编程的。 我的意思是,让 iPhone 屏幕上的内容动起来, 比我之前摆弄的终端应用要神奇得多。 所以,我的意思是,继续为此做出贡献, 也许有一天我就能“毕业”回去, 重新开始开发应用了。 是的。当我们在视频结尾时, 很多时候我们总会说:“我们迫 不及待想看看你们会开发出什么。 ”这真的不是说说而已。最令人欣喜的事情 之一就是:当你花了一整年时间 研究和构建这个 API, 脑海中已经构想出 人们将如何使用它,然后看到 有人说“我用这个 API 做了这个”, 你会不禁感叹:“哦,原来还能这样。
”那感觉真棒。 起初你会想:“我没想到会是这样。 ”但他们实际上做出的东西真的很酷, 这确实意味着我们实现了目标—— 打造了一个组合式 API, 它通常能以出乎意料的方式运作, 让你能实现意想不到的功能。 出乎意料。太棒了。 不,这个框架的演变过程令人欣喜, 不仅在于它变得多么成熟, 更在于它带来的愉悦感。 我特别喜欢这样的活动, 让我们能与现场和线上的开发者互动, 提出问题,并真正了解 这些 API 被如何使用、 哪些方面做得出色, 以及它们如何能更好地满足你们应用的需求。 所以我认为这非常有成就感。 我在此代表我们所有人发言:这 确实很有成就感,真的非常有成就感。
这是 Slido 上提出的一个问题。 很多人都在询问 SwiftUI 中推荐的应用架构。 您能就此多分享一些吗? 我们经常收到 这个问题。我认为, 与过去那种试图推荐一种架构、 宣称“一刀切”方案,然后引导大家 沿用该架构的做法相比, 如今的情况已经有了很大变化。 事实证明,也许你的应用是高度服务器驱动的。 因此,那种架构对你来说可能并不是最佳选择。 因此, SwiftUI 的真正 目标是提供每种架构都需要的构建模块。 我们非常清楚社区正在构建的各种架构。 我想特别强调一点:当你采用 基于 ViewModel 的架构 (例如 Observable 所使用的)时, `at Observable` 宏 可以作为你的起点。 如果你采用的是基于 ViewModel 的架构,那就使用 `at Observable`, 利用这个原子级构建块及其与 SwiftUI 的 原生集成来构建你的 ViewModel。 此外, `Observable` 还能 在不损失通用性的情况下 在 UKit 中使用,这一点也非常棒。 而强化这一理念的,正是互操作性—— 这也是我们的关键目标之一。 我的意思是,我们看到了 AllTrails 的案例, 他们是如何逐步采用 SwiftUI 的。 对于许多应用来说,情况确实如此。 从应用架构的角度来看,这些应用 各自的起点各不相同。 而 SwiftUI 能够适应 任何应用的既有架构, 这一点确实至关重要。 是的。即使在 Apple 内部, 也有许多应用依赖于这种互操作性。 没错,确实如此。 这确实是关键所在—— 它能让你的团队找到与每位成员的共鸣点, 契合他们偏好的开发方式, 并实现他们心中最优雅的解决方案。 相关资源非常丰富, 比如如果你的应用目前仍基于 UIKit 或 MapKit, 而你正在考虑何时踏入 SwiftUI 的采用领域。 开发者网站上有大量资源, 还有 WWDC 视频, 可以引导你完成这个过程。 这是一条已被广泛探索的道路, 我认为有大量优秀的资源可以助你迈出第一步。 是的。
我这里还有另一个来自 Slido 的问题。 这个问题更偏宏观层面。 很多人都在询问具体的问题, 比如“在特定平台上, 实现这种行为的 API 是什么? ”或者“我该如何在视图中实现这一功能? ”而我更想从更高层面了解, 您建议大家如何查找 SwiftUI 中不同行为的 API? 您推荐哪些工具?
我一直以来在将各种模型 集成到 Xcode 方面都取得了很大成功。 至少,这是一个很好的起点。 你可以随时用任何语言进行描述。 实际上,我虽然没见过这些模型 在不同语言中的测试结果,但你只需用 Natural Language 描述 你要找的内容, 随即就会得到一堆修饰词和建议。 这确实是一个非常有帮助的起点。 是的。这些智能功能非常酷, 确实大大降低了原型设计的门槛。 我发现这特别适合用来进入 某个自己不太熟悉的领域, 然后进一步加深理解。 从这方面来说,最新版本确实很棒, 但我认为有一点很重要: 虽然它会生成结果,会尝试向你 展示某些内容,但作为开发者, 你的职责是对此提出质疑, 并确保自己理解它的运作原理。 我们都熟悉幻觉之类的东西。 因此,除了验证这一点之外, 我认为它还是一种强大的学习工具—— 这正是你开篇提到的,这很好。 它建议使用这一组修饰符来实现这种效果。 我是否真正理解了它们各自的作用? 这会引导你查阅文档。 我们的目标之一是确保每篇文档 都附带一小段示例代码。 这样你就可以通过这种学习方式, 自己开始构建一个心理模型, 我觉得这很酷。 是的。这也是我们今天聚在这里的目的。 基础知识——我觉得今天我已经 说了大概十次这个词,也许更多。 我们来数一数。但花 时间去理解基础知识, 能帮助你对自己的代码更有信心。 而且,如果你不理解某些内容, 也许 LMM 会生成相关内容。 你仍然可以利用这段时间, 去接触那些你目前还不 太有把握的领域,并加以学习。 所以这真的很棒。 这是其中一点。 是的,尤其是在你尝试学习的时候。 我几乎从不一边学习一边编写应用。 比如,我总是通过 Xcode 的预览功能来学习。 然后,一旦你打好了基础, 再将它应用到实际应用中——尽管那里 同时存在各种副作用和复杂情况,但至少你 已经掌握了它应该如何运作的模型。 所以当事情看起来不对劲, 或者你可能做了些奇怪的操作时, 你知道该往哪里找问题,该从哪里着手。 是的。预览功能是我最喜欢的工具之一, 不仅仅是因为它能轻松隔离、 定位和复现 bug。 比如遇到一些奇怪的行为, 我只需快速注入状态就能观察其表现;此外, 在快速原型设计和布局方面, 我特别喜欢用预览功能来处理布局。 这是我最喜欢的工具之一。 另一个刚浮现在我脑海中的资源是开发者论坛。 除了文档和 WWDC 视频之外—— 这些不仅让我了解不同的 API, 还能在示例应用的更广泛背景下观察它们, 并了解各个部分是如何结合在一起的。 比如那个 ScrollView 视频就太棒了, 因为它给了我很多 关于动画的灵感, 但开发者论坛也是个绝佳的资源, 因为你可以按框架(如 SwiftUI 或 UIKit)提问, 也可以提出针对特定平台(如 watchOS 或 Vision OS)的问题。 即使当下没有具体问题, 也许你只是想学习,并 了解未来可能需要考虑的各种方向。 这是一个绝佳的资源, 而且由 Apple 工程师 和支持团队负责管理。 因此,在您作为开发者进行开发 并持续成长的过程中,这是一个绝佳的参考资源。
我还想稍微谈谈 SwiftUI 如何借助 Liquid Glass 帮助 应用保持现代化—— 这是 iOS 26 及其他操作 系统更新中的重要内容。 您能否详细介绍一下 SwiftUI 是如何帮助应用 在苹果平台上保持现代化的? 你愿意分享吗? 我的意思是,首先我可以简单说一下, SwiftUI 提供了很多 更高层次的语义 API, 其层次甚至比过去 UIKit 和 AppKit 提供的还要高, 这让我们能够提供更大的灵活性。 因此,当你的应用基于这些 语义概念进行实现时——比如, 即使在 Liquid Glass 推出之前,动态类型、 深色模式、这些自适应概念发生变化时, 你的应用也能自动更新。 Liquid Glass 不仅拥有许多出色的组件, 在很多方面它本身也是一种主题。 因此,它并不一定会改变应用的核心结构。 通过这些语义概念进行实现,应用就能自动更新。
此外,它同样适用于自定义控件。 所以即使你创建了自定义控件, 也可能需要进行一些适配。 但还有其他 API, 比如 GlassEffectContainer, 特别是 SwiftUI 凭借其 极强的可组合性 API, 实现了其他框架无法企及的功能。 GlassEffectContainer 的 API 接口非常精简。 GlassEffectContainer 只有 一个视图。 它只有——我想——一个视图。 然后可能还有 3 到 4 个公开的修饰符。 然而,它却能做到这样。 它拥有所有这些过渡效果, 以及能够融合、变形的组件。 你可以想象,一个命令式 UI 框架要提供如此简洁、 易懂且充分利用 现有组件的 API, 恐怕会很吃力。 所以我认为 SwiftUI 确实 为我们的 Liquid Glass 提供了很大帮助。 我觉得这又回到了 Nick 提到的那句话:“学一次, 用遍各处”。这不仅适用于不同框架之间, 甚至在这个概念 内部也是如此—— 你为其他目的学到的所有动画技巧, 在这个场景中同样适用。 所以我认为这是该原则 在实践中又一个很好的例证。 是的。对我来说,这就像知道何时该使用容器, 以及了解那些——再次强调——基础知识。 这些基础知识会不断被用 到:知道该运用哪些基础, 然后准确把握在何处融入品牌 元素或突出应用的独特 之处,并确保这些设计经得起时间的考验, 因为操作系统在不断演进, 你希望确保自己 不会陷入因应用架构不当而被迫 投入大量精力进行修改。 因此,如果你能将这些 独特的体验恰当地融入到合适的位置, 那么你就能…… 比如,如果你设计了一个非常花哨的动画效果。 当你点击“点赞”按钮时, 某个元素会弹起、旋转并舞动。 那么这种效果能立即在所有平台上实现, 且非常稳健,不易受变更影响, 也具备很强的未来适应性。 是的,在与开发者举办的“ Liquid Glass”研讨会上, 有一点让我印象深刻: 如果他们使用了原生组件(如 Tab View)并采用了系统控件, 那么只需针对 iOS 26 重新编译,无论应用是基于 UIKit 还是 SwiftUI, 都能自动将应用调整到最佳状态, 从而开始利用“Liquid Glass ”效果,在 Apple 平台上呈现现代感,并且。 随后,开发者便可以开始进行更高层次的优化, 并考虑重新审视导航或品牌设计等机会。 这真的非常酷。 感觉有点像魔法一样。 而且我认为,作 为 SwiftUI 框架的基础, 它消除了保持现代感并充分 利用操作系统先进功能的障碍,这一点真的很棒。 我认为我们考察过的一个指标 是:仅仅为了让应用 看起来像是平台的一部分而花费的时间。 我们希望尽可能减少这部分时间, 这样你就能把所有精力都投入 到真正让应用独具特色的地方。 我认为这是展示这一点的绝佳方式。 是的,完全同意。你能再多谈谈系统组件吗? 比如,我认为今天的示例 应用“Wishlist” 中确实有很多值得探讨的地方, Majod 在她的设计演示中也讨论过这一点。 其中有很多核心系统组件,比如标签页( Tab View),我们通过使用 SF Symbols 作为图标, 并让应用顺应熟悉的标准系统架构, 使其设计非常简洁。 但同时,我们通过不同字体的排版来展现 个性与品牌特色。 你能谈谈你建议大家如何权衡这些取舍吗? 即何时使用内置控件,何时又该展现更多个性?
哦,这个问题问得好。
天哪。 这确实有很多要探讨的地方,因为这取决 于具体情况。 是的。 确实如此。我本来想说,这确实是一个设计决策, 比如,你想让应用在哪些方面独树一帜? 又希望它在哪些方面给人熟悉感? 我认为熟悉感是有好处的,因为对于第一次 下载你应用的用户来说,你希望他们能感到熟悉, 而不必去学习如何使用这款独特应用。 然后还有那些能吸引用户的小细节,对吧? 正是这些让应用脱颖而出。 这是一个很好的视角。 我觉得可以这样来看待这个问题:比如, Russell 做了大量相关工作。 他构建了大量的 UI 呈现 控制器、弹出视图以及 SwiftUI 弹出视图,而弹出视图正是我 最喜欢的组件之一,因为正如泰勒所说, 人们已经知道它会如何工作。 这是操作系统如此核心的一部分, 以至于我真的犹豫 是否该给出通用的指导建议。 但尽量不要自己 实现弹出视图。 对吧? 是的,确实如此。 不,不是那个。 那是值得信赖的。而且不仅对我来说如此。 不仅对你来说如此。好的。 我就相信你说的。嗯。 我觉得过去几个问题中有一个主题, 就是渐进式披露, 无论是从用户的角度来看, 还是从API的角度来看,比如 如果你使用更高层次的语义 组件,那么它不仅对你自己, 对所有使用该应用的人来说都会更加熟悉, 而且适应性也会更强。 但在 API 方面也是如此, 比如如果你使用更高层次的语义组件, 那么它不仅对你和所有应用 用户来说会更熟悉,适应性也会更强。 而且对于你作为开发者来说也更容易理解, 因为这是一个更简单、 更高层次的概念。 如果你深入到为你的用例对该 组件进行越来越多的定制,那么对每个人来说, 它就会变得更加复杂——也许这是合理的, 但我们希望确保存在一种连续性。 因此,增加更多 适当定制高级组件的方法, 以便你不必完全深入细节, 这一点非常重要,而且我认为我正 在将这一点与我们 刚才讨论的最后一点联系起来。 但不,我感受到了,我确实感受到了。 我的意思是,我想到的是, 系统组件其实也有很多可以发挥的空间, 比如谈论弹出视图, 以及 iOS 26 中的一些精妙行为, 比如背景会根据弹出视图的高度略微变化。 有很多现成的工具可以用来自定义弹出 视图的行为和修饰符。所以有时候你可以…… 不过需要花点时间阅读文档, 才能弄清楚有哪些选项。 比如你可以设置 presentation detente —— 是 detente 还是 detent?我总是搞混,这 就是我的秘密。 我总是搞混这个。 这个API的创建。 Word。 Detente。 好吧,我放慢点说。 是 detent。哦,原来是。看,我。 就像一个微量移液管。 我当时就是。 就像一个。 微量移液管。 不过,你知道的, API 内置了大量功能, 可以实现非常复杂的行为, 还能自定义这些内置的导航 工具和控件,让你的应用展现出独特个性。 比如视图修饰符、字体, 利用这些系统组件,你能做的事情实在太多了。 当然,目前仍存在一些限制,我们希望你 能去探索这些限制,发现它们并告诉我们。 我们会认真倾听,以便消除这些限制, 让它变得更加强大。 这绝对是。 千真万确。 她设计中我最喜欢的一点, 尤其是 Liquid Glass 效果,是—— 你可能需要纠正我——随着弹出视图变高, 它的不透明度会逐渐增加。 但弹出视图本身是玻璃材质。 而且,根据弹出视图的高度不同, 它会紧贴设备边框,或者根据大小浮在屏幕上。 这简直,它非常、非常…… 真棒。这些年来,我们为薄片 添加了许多新特性。你是“Sheet 之王”吗?是这样吗? 不。其实,我们
有很多人都在致力于优化 Sheet, 让这个组件更好地工作。 是的。 不,它真的很美。 看到这么多系统组件在 iOS、 第26 版操作系统以及 Liquid Glass 的加持 下被如此精心重塑,真的令人欣喜。 对我来说,体验 Cat 之前 提到的那种乐趣也很有趣—— 比如看到一个你觉得很棒的应用, 然后进行这样的思考:这看起来真酷。 我要试着用 SwiftUI 自己实现这个功能。 这有点像“空白页难题”。 比如,你可能想提升布局或数据流方面的技能, 却不知道该从何入手。 你会疑惑:我需要设计稿吗? 你知道,这其实是你需要解决的第一个问题。 但我认为,通过这种独立练习, 尝试从零开始自己构建一些东西, 是积累经验并巩固所学知识的绝佳方式。 是啊。 确实如此。前几天我就用 iOS 主屏幕上的应用尝试了 “应用抖动”动画效果。 就像…… 你长按某个图标时。 对。它们会进入编辑模式,然后…… 三个图标都会一起移动。 我想看看谁能做得最好。 效果是随机的,真的很难做到完美。 我觉得是拉塞尔。没错。 拉塞尔。 这不仅仅是一个重复的曲线。 所以,在 SwiftUI 中用正方形实现 这个效果,其实是个挺有趣的练习。 我给你个提示。 我相当确定你们 会用到自定义动画中那个高级的“ should merge”属性。 我当时想到了 baz 动画。 好的,很棒。是的。在关于 动画的 dub dub 视频里提到了这一点。 那确实是…… 使用 Phase 可能也是 一个非常可行的实现方式。 看吧。解决一个问题也有多种方法。 我觉得这真的很有趣。 有时我会尝试重做某个东西, 也许一开始采用一种方法,效果还不错, 然后我继续深入,在这个过程中不断学习, 这些循序渐进的方法。 我觉得这又回到了罗素 提到的“渐进式披露”概念, 这几乎是 API 设计本身的核心原则, 但它更像是推动自己迈向下一层—— 比如你尝试做某件事,哦,已经很接近了。 接下来你可以尝试学习和探索什么, 才能让它更接近你的预期,或者为它注入新的、 独特的元素? 是的,我觉得无论一 个人对 SwiftUI 有多少经验, 无论是刚入门,还是希望 巩固现有理解,总有 更多东西可以学习和发现, 既包括新的 API, 也包括寻找更有趣的实现方式。 是啊,太棒了。那么, 我最后还有一个问题想问在座的各位。 今天我们聊了很多关于“愿望 清单”的话题,还有各种旅行计划, 以及我们在这些旅途中想做的事情。 各种活动,各种旅行。 你们下一次的假期计划是什么?
我非常希望能在今年去苏格兰看看。 我从来、从来没去过那里。 我想看看那里的乡村风光,还想体验 一下靠左行驶的感觉。那是你的一个梦想啊。 学点苏格兰俚语吧。
你可以的。“You can, You can。”这是什么意思? 我觉得这大概就是……你知道的。 好,又学到 新知识了。 等你到了那里,得试着用用看,看看管不管用。 如果观众中有谁知道的话。 来找我。 在联谊会上。 太酷了。 其实我不知道我的…… 好,向现场所有的射手座朋友们打个招呼。 我的……我的生日在假期期间, 所以我姐姐计划了本月晚些 时候的一次延期的惊喜旅行, 我甚至不知道我们要去哪里,所以到时候再看吧。 这真是一场冒险。还有“愿望清单”, 你可以把所有活动都标记成问号。 这就像一个随机数生成器。你永远不 知道接下来会发生什么。 这可能就是未来。 是啊,是啊。 所以,我就是那种不喜欢在假期里放松的人。 虽然这听起来很讽刺,但我确信。 你是个喜欢动起来度假的人。 我就是那种喜欢动起来度假的人。 所以我们喜欢尝试不同的徒步路线。 接下来我们计划之一是, 想在意大利的多洛米蒂山进行一次多日徒步。 这是其中之一。目前还没有具体计划。 我们正打算确定具体什么时候能成行。 不错。听起来很棒。 我们大概有两次国际旅行, 还有一趟神秘之旅,拉塞尔。 也许是你去库比蒂诺之类的行程。 我想我们很快就会知道的。 是啊,我们得尽快跟进一下。 好吧,非常感谢大家。 大家能和我一起为他们鼓掌吗? 这次活动让我受益匪浅, 讨论 SwiftUI 真的非常有趣。 谢谢大家。
深入了解 SwiftUI 真的很有趣, 不仅在于技术细节,更在于框架背后的故事。
今天真是充实的一天。 我们从视觉层面的设计、 布局和动画,一直到数据流, 全面探讨了 SwiftUI 的基础概念。 在每个环节,我们都结合了 像 Wishlist 这样的真实应用, 解析了设计决策背后的理论依据; 詹姆斯 · 格雷厄姆(James Graham )还通过亲身经历, 分享了 SwiftUI 如何 帮助 AllTrails 在所有章节中更高效地发布和维护功能。 我希望大家能牢记一点:请花时间投入 并学习 SwiftUI 的基础知识。 这将帮助你更好地运用该框架,打造出色的应用。
我和我的同事 们非常感谢大家今天在 Slido 上抽出时间与我们一起学习。 我们回答了 200 多个问题,若想继续交流, 请访问 Apple 开发者论坛,在那里你 可以就 SwiftUI 及其他话题提问。
“Wishlist”应用的源代码将 在未来几周内开放下载。 无论你是正在规划下一次旅行, 还是想重温今天演示 中的部分练习,这都是一款非常有趣的应用。
我和同事们在演讲中提到了 某些视频和文档,今天稍晚些时候, 我们会发送一封包含这些资源链接的邮件, 以便大家继续学习。
了解 SwiftUI 及其他 技术的最佳方式之一, 就是观看 WWDC 的视频。
那里有数百个视频, 大家可以在 Apple 开发者网站 或 Apple Developer App 中观看。你们甚至可能会在这些视频 中看到今天演讲中的一些熟悉面孔。
今天真是精彩纷呈的一天。 但在结束之前,我还有一位特别嘉宾要介绍。
你们可能曾在主题演讲中见过她, 或者在最近的 Swift 学生 挑战赛启动仪式上也见过她。 她将发表简短讲话, 并帮助我们为本次活动画上句号。 请和我一起欢迎 Apple 开发者 关系副总裁苏珊 · 普雷斯科特。
大家好!
我非常兴奋。 首先,是因为我的工作。 希望你们也都热爱自己的工作。 但正如你们所见,这份工作真的超级有趣。 希望今天这一整天, 还有许多今天没有参与其中的人。 在这个组织中, 全球各地的同仁都对开发者 社区怀有满腔热情和坚定承诺, 希望今天是大家能真切感受到这一点的一天。 我想感谢大家的光临——虽然 已经听过许多人的致谢了。我要感谢所有 登台分享的嘉宾,包括来自 AllTrails 的詹姆斯— —他今天能来并分享自己的经验, 值得获得一颗小金星。 所以,我不得不问:今天对大家来说 是美好的一天吗? 非正式调查。大家觉得怎么样?
我们非得按《罗伯特议事规则》来吗? 现在我得请想嘘声的人先离开, 还是我们可以直接跳过这一步? 我很高兴能感受到现场的热情。 我能感受到观众们传递的热情。 我真的非常喜欢这种氛围。 正如莉亚稍后会详细说明的, 活动结束后我们还有个环节。 对于现场的各位, 我们同样非常感谢线上参与的各位, 你们的参与意义重大。 所以,嘿,我们爱你们。 你们得自己去倒一杯果汁, 因为这里会为每个人准备调味饮料。 没错,现场还会提供一些轻食。 但我认为最令人兴奋的是,你们在台上看到的人, 以及苹果工程和开发者关系团队的许多其他同事, 都来到这里与你们交流。 大家也有机会互相认识, 因为这个社区并不是——你们知道的— —以 Apple 为中心的辐射式结构。 而是由你们大家共同构成的。 而我们也会在力所能及的范围内参与其中。 总之,接下来我将把话筒交给丽莎、 丽莎,最后由莉亚来作总结。 我只想说声谢谢,稍后在交流会上见。 谢谢大家。 是的。
谢谢大家。 非常感谢大家今天来到库比蒂诺现场或在线参与。 你们是我们开发者社区的灵魂与核心。 希望你们学到了新知识, 并为接下来的探索找到了灵感。 对于现场参与的各位, 稍后我们将在室外聚会,享用茶点, 并有机会与Apple的工程师和设计师交流。 致在场的各位,无论是在现场还是在线上。 我们希望你们能很快再次加入我们, 无论是在线上,还是在您 附近的 Apple 开发者中心。谢谢。
-