-
SwiftUI 数据流
了解用于在 SwiftUI 中管理数据流的工具和技术。探索如何使用属性包装器对数据进行建模,充分利用 @Observable 的强大功能,以及在 App 中实现数据持久性。
这个讲座最初作为“与 Apple 会面交流”活动“SwiftUI 基础知识:使用 SwiftUI 构建卓越的 App”的一部分讲授。请观看完整视频,以了解更多见解和相关讲座。资源
-
搜索此视频…
大家下午好。 我叫 Cole,是 Apple 公司 核心技术布道师。 今天,我将探讨 App 中的数据, 以及如何将数据流向 SwiftUI 视图。 “愿望清单”App 是一个很好的示例, 今天我将通过它来演示 在 SwiftUI App 中 可能遇到的一些数据流方式。
“愿望清单”App 在数据处理 方面做了许多与你们的 App 可能需要做的事情 相似的操作,并且会根据 具体的数据类型采取不同的处理方式。
我将先讲解其中一些最重要的内容。 在这个 App 中, 某些地方当用户与界面交互时, 界面的状态会发生变化。 例如,当用户修改行程时, App 需要跟踪 UI 是否处于编辑模式。
这种情况会在几个不同的地方出现, 比如跟踪点击按钮后弹出表单的时机, 或者需要显示提示框的时机。 我通常将这些情况统称为“视图状态”。
此外,该 App 中还包含一个数据模型。
“愿望清单”App 的目标 是展示我那些精彩行程和活动中的所有丰富数据。 例如,这次京都之旅就 列出了许多当地的有趣活动, 如探索寺庙、 日出时分漫步运河小径等等。 因此,我需要在 App 中对这些数据进行建模, 并将其流式传输到所有的 SwiftUI 视图中。
此外, App 还需要记录用户 在 App 中设置的偏好选项。 例如,在行程的活动 列表中, App 提供了一个排序按钮。 用户可以按标题、日期或活动完成状态进行排序, App 需要记录用户上次选择的排序方式, 以便下次显示该视图时使用相同的排序方式。
最后,我想构建一个 App ——不,我是说, 我想设计一种持久化方案。 持久化是一种允许 App 将数据保存 到设备存储(例如文件中)的方法。 这样,当用户下次返回 App 时, 即使 App 被重新启动,所有更改仍会保留。
因此,在本节中,我将更详细地讨论这些用例。 内容包括视图状态数据、构建丰富的数据模型、 处理偏好设置和配置,以及几种持久化技术。
本节结束时, 您将能够对这些用例进行分析, 并掌握可复用的模式, 以满足您自己 App 中的类似需求。 首先,我将从视图状态 (ViewState)开始讲起。
正如我之前 提到的,有时 App 只需创建一 段数据来跟踪界面本身的状态。 这些数据通常是临时性的, 当视图消失时重置它们也是安全的。 例如,示例 App 会跟踪 某趟行程是否处于编辑模式。 如果用户离开该行程页面, 随后又返回, 那么用户界面不再处于编辑模式是合理的。 即使我没有点击“完成”按钮。
这里还有另一个需要在界面中跟踪状态的例子。 在这个 App 的“愿望清单”标签页中, 工具栏里有一个“添加”按钮。 刚才Kurt在讨论优化动画时提到了这个按钮。 点击该按钮会弹出一个弹出层,用户可以 在其中开始输入新 行程的详细信息以添加到 App 中。
因此, App 需要一个状态来跟踪 该弹出层当前是否显示在屏幕上。
此外,弹出层本身还包含用于 保存更改或取消添加行程的按钮。 因此,弹出窗口中的这些 操作需要一种方式来更新状态, 以便 SwiftUI 能关闭该弹出窗口。
下面将演示如何在 SwiftUI 中实现 这个“添加”按钮,并将弹出窗口放置
在“愿望清单”视图的主体中。 这里有一个带有符号标签的 SwiftUI 按钮,我需要在按钮的动作中添加一些代码,
以显示弹出窗口。 我需要使用“`sheet(isPresented:)`修饰符,并将它添加到视图中。
现在,在完成这段代码之前, SwiftUI 需要我做出一个决定。 究竟该用什么数据来追踪 这个弹出层当前是否实际显示在屏幕上呢?
其实,这正是使用 `@State` API的绝佳用例。其工作 原理如下:我将创建一个名为 ``isPresentingAddTrip`` 的新属性, 并使用 `@State` 对其进行修饰, 其默认值为 `false`。
使用 `@State` 会告诉 SwiftUI 创建一个与该 视图生命周期相同的新数据项。
关于该值的生命周期,我稍后会详细说明, 但目前,既然已经定义了该属性, 我就可以在视图的其余部分中使用它。
在操作按钮的动作中,我将把 `isPresentingAddTrip` 设置为 `true`, 因为点击按钮时应始终显示弹出表单; 如果弹出表单已显示, 则通过将状态传递给 `disabled` 修饰符来禁用该按钮。
请注意,由于视图主体会读取 `isPresentingAddTrip` 的 值,就像在此修饰符中一样。 愿望清单视图会在任何时候都依赖于此值。 当“`isPresentingAddTrip`” 发生变化时, SwiftUI 会更新此视图。
我还将“`isPresentingAddTrip`” 属性传递给 sheet 修饰符。这种美元符号表示法 允许我将绑定传递给该状态。 让这段代码与 sheet 共享该值, 以便它既能读取该值, 也能对其进行修改。 稍后我会回来详细解释绑定。
用 `@State` 修饰一个属 性,会告诉 SwiftUI 该属性由外围视图拥有。
只要视图存在,该属性的值就存在,
这是我刚才提到的一个重要点。 状态属性的生命周期与视图 在接口中的生命周期一致, 而非视图结构体本身的生命周期。 因此,我将进一步探讨其实际工作原理。
正如 Leah 今天早些时候所讨论的, SwiftUI 视图只是短暂 存在的描述,就像一个模板。 当“愿望清单”视图在界面中出现时, SwiftUI 会运行其 主体以更新屏幕上的 UI, 但随后会丢弃该实例,
并且每次 SwiftUI 渲染该 视图时都会重复这一过程。
那么,既然默认值为 false, 为什么在该视图每次被重新初始化时, “`isPresentingAddTrip`” 属性不会被重置呢?
这是因为 SwiftUI 会对像这样处 于 `@State` 属性给予特殊处理。
在幕后, SwiftUI 拥有自己的数据存储空间, 用于存放所有视图所拥有的状态。 当 SwiftUI 首次实例化该视图时, 它会在内部存储中为该属性分配空间, 并将其与其他视图所拥有的所有状态一同存放。
该存储空间通过视图的标识符进行索引。
可以将这个标识视为该视图 在屏幕上渲染结果的生命周期。 因此,当 SwiftUI 首次渲染该视图时, 会为其分配一个标识。
当用户离开该视图时, SwiftUI 会从其存储中删除该条目。 如果用户稍后返回该视图, SwiftUI 会分配一个新的标识, 并在存储中分配一个新的条目。
下面将演示这一机制如何 应用于我的按钮:当用户首次 导航至“愿望清单”视图并点击该按钮时, 会弹出一个弹出视图。 SwiftUI 为该视图分配存储空间。 该视图通过默认值为 false 的 `isPresenting` 属性来表示 “正在呈现”状态。
随后,它实例化该结构体并设置 `isPresenting` 的值。 `isPresenting` “Add trip”状态, 使其与 SwiftUI 的内部 存储一致,随后执行 “wishlist”视图的主体代码以
在屏幕上渲染内容;当用户点击该按钮时, SwiftUI 会丢弃该视图实例。 操作闭包执行时,将 `isPresentingAddTrip` 设置为 true, 但由于这是状态属性, 该赋值操作会修改 SwiftUI 的内部存储; 且由于视图的主体代码依赖于此状态, SwiftUI 会触发更新。 创建一个新的视图结构体, 并从内部存储中获取新的 true 值来 填充 `isPresentingAddTrip` 的值。 在运行 `body` 之前。
因此,这种内部存储机制就是 SwiftUI 在界面中如何为视图的生命周期维护状态的方式。 尽管这些结构体本身是瞬时的。
好的,因此这个内部存储解释了 为何该 `@State` 属性 会要求 SwiftUI 在界面中 跟踪该视图生命周期内的布尔值。
但 `@State` 不仅 在单个视图声明中有用。 它还可以通过所谓的“绑定”与其他视图共享。
我在视图修饰符中的这个美元 符号允许我访问一个绑定。 这是“添加行程”视图的状态。
绑定只是对 App 中某个状态的读写引用。
当你创建一个需要从其外层视图读取值, 并且可能还需要修改 该值的视图时,绑定非常有用。
绑定在 SwiftUI 中随处可见。 你会在切换按钮、文本字段以及弹出 视图修饰符等的参数中找到它们。
这些是封装好的控件,它们依赖于这些数据,并 在需要时可以修改数据。 例如,这是“添加行程”视图的代码片段, 该视图位于弹出视图内部。
它使用一个绑定,从父视图接收对状态的引用, 该状态用于跟踪弹出视图是否在视图主体中显示。 其中包含一些按钮,可以触发弹出视图的关闭。 为了实现这一点,它们在操作 闭包中将 `isPresented` 绑定的值设置为 false。
这会促使 SwiftUI 更新 所有依赖该状态的视图, 比如我的视图。 关闭视图后,弹出层便会被收起。
像我的弹出层这样,仅需 跟踪用户何时与界面交互的场景,非常适合使用 `@State` 非常适合。 这是临时数据,应在视图消失时重置, 或者用于那些仅在视图存在期间才应存在的对象。
但对于其他类型的数据(比如我的数据模型), `@State` 并不是最佳选择。 在这些情况下,你需要一种方式向 SwiftUI 表达由代码其他部分拥有 (而非 UI 本身拥有)的状态类型。
这便引出了我的下一个用例, 即“愿望清单”中的数据模型。 数据模型是这款 App 的核心与灵魂。 它包含了用户希望通过 App 获取的所有信息, 例如每次旅行的名称、照片和活动内容。 就像这个示例 中所示,旅行对象名为“秘鲁越野之旅”, 并关联了一张绝美的照片。
这些对象之间还存在关联关系。 例如,一次旅行可以包含多个活动, 而活动本身也有自己的属性, 如名称和一个布尔值, 用于标记该活动是否已完成。
因此,我使用类创建了一个数据模型, 用来表示该 App 中的旅行和活动。
每个类都有属性来存储名称、照片和日期等信息。
它们还存储了彼此的引用, 以便建模这些对象之间的关系。 例如,旅行对象可以通过 在 activities 属性中存储对相关活动的引用, 与任意数量的活动建立关联。
这些对象也是可编辑的, 因为用户可能会在用户界面中修改这些属性。 比如当用户在文本框中编辑旅行名称时。
我还创建了一个名 名为 DataSource 的类, 用于管理 App 中的所有这些对象。 这是一个集中点, App 可以通过它 查询所有需要在 UI 中显示的对象, 例如获取所有行程的列表。
因此,拥有这样一个数据模型是一个很好的开始。 但要想在 SwiftUI 中 真正使用这个数据模型, 我确实需要添加一个简单的功能。
数据模型需要能够感知其属性发生变化时, 以便 SwiftUI 更新 UI。
而这就是 `observable` 宏的作用。 仅需这一行代码, 你就能让 SwiftUI 能够与类中的属性 建立依赖关系。
`observable` 宏为每个 类的属性添加了更新跟踪功能。 要使用它,只需用 `@Observable` 宏修饰每个类即可。
现在数据模型已具备可观察性, SwiftUI 便能直接 与模型属性建立依赖关系。
在此示例中, `tripcard` 视图获取了 应显示的 `trip` 对象的引用。
请注意,这里没有 `@State` 或 `@Binding`。 事实上,这个引用上根本没有任何修饰。 使该视图正常工作的关键 在于:在视图主体内部,它读取了 `trip` 的 `photo URL` 属性。 这告诉 SwiftUI, 每当该可 观察类上的 `photo URL` 属性发生变化时, `tripcard` 视图就需要更新。
这种机制同样适用于计算属性。 例如,假设我想为尚未 保存照片的行程使用占位图。 为了在 UI 中处理这种情况。 我可以在 `Trip` 上定义一个名为 `photo URL` 或 `placeholder` 的计算属性。 如果该行程未定义照片, 该计算属性将返回一张占位图。 然后,我在视图主体中使用这个新的计算属性, 而不是 `photo URL`。
当访问该计算属性时, SwiftUI 能够确定 视图主体中读取的是底层属 性 `photo URL`。 因此,每当底层“照片 URL”发生变化时, SwiftUI 都会更新行程卡片。
好的,总结一下, 这个 App 使用了一种由行程和活动 对象组成的结构化数据模型。
它使用引用类型或类来建模每个对象, 这样我就可以通过引用来建模 这些对象之间的关系, 并且这些属性在UI的任何位置都易于编辑。
为了让这些类中的数据流向 SwiftUI, 我只需在每个类的定义中添加 observable 宏。 这样,我的 SwiftUI 视图就 可以在视图主体中直接读取这些属性了。
现在还有一件事需要处理。 到目前为止,我演示的内容运行得非常顺利。 一旦视图已经拥有了对某个对象 (例如 `trip`)的引用。
刚才我提到,所有这些对象都由数据源类拥有, 但如何告诉 视图应该使用哪个数据源实例呢?
其实,我之前已经讨论过实现这一点的一种方法, 它始于 `@State`。
回想一下,在 `@State` 中, 我们会创建一个与视图 生命周期相同的对象,因此在我 App 的顶层, 我可以创建一个名为 `@State` 的属 性来保存数据源对象。 将 `@State` 放入此 App 声明中,会创建一个与 App 生命周期相同的对象,然后将其传递给各个视图。
不过,这种方法可能只适用于 非常简单的视图层次结构。在这个 App 中, UI 的不同部分可能需要访问数据源。 例如,要获取完整的行程列表, 且视图层次结构非常简单时, 直接将数据源对象传递给需要它的视图 是完全合理的。
但随着 App 复杂度的增加, 这种方法可能会变得繁琐。 一个拥有多层嵌套视图的 App, 最终可能需要传递并存储数据源的引用, 仅仅是为了将其传递给需要它的更深层视图。
因此,为避免 这个问题,此类场景非常适合使用环境。
可以将环境视为配置 App 各部分的核心属性, 且这些属性通常不会频繁更改。
在此情况下,我将创建一个数据源对象作为状态, 并将其设置到视图的环境中。 这样就为需要数据源的视图 配置了这个特定的实例。
这种做法是安全的, 因为数据源对象本身不会频繁被替换。 它是可观察的, 而且我的 App 中有 很多不同的视图可能需要引用它。 因此,通过将数据源放入环境中, 任何需要引用它的视图都可以直接获取。
环境会流经其所应用的整个视图层次结构, 你只需从环境中请求一个值, SwiftUI 就会提供它。
例如,在视图层次 结构的更深处,最近的行程 页面视图通过在属性上添加 `@Environment`。 SwiftUI 会将该引用 填充为环境中类型匹配的对象。
由于数据源是可观察的, 视图可以直接在视图 主体内对其属性建立依赖关系。 在此处,"最近添加的行程" 属性是在该视图主体中读取的, 因此每当 App 中添加新的行程时,"最近 行程"页面视图都会自动更新。
因此,视图状态和我的数据模型目前已涵盖了该 App 中大部分所需的数据流,但还有其他 几种用例需要采取略有不同的处理方式。 接下来,我将继续介绍该 App 中的偏好设置和配置。
我之前提到过,当用户点击某次旅行时, 可以按名称或是否已完成对显示的活动 列表进行排序。
App 需要存储这一偏好设置, 以便用户每次进入该视图时, 都能采用其首选的排序方式。
不过,将偏好设置 纳入我之前讨论的数据模型并不太合适, 因为这其实与行程本身并无直接关联。 它只是用来追踪用户更倾向 于如何使用 App 的不同功能。
因此,要实现这 一排序功能,我可以先定义一个新的状态, 用于记录当前使用的排序方式。
在本例中,它被称为“排序选项” (sort option)。 接着是 Activity。 子视图在布局其活动时会读取该排序选项。
这样一来,活动便会根据用户在菜单 中的选择进行排序。
但请注意,像这样的 `@State` 属性仅 在视图的生命周期内存储。 因此,如果用户离开该视图后再次返回, 排序方式将始终恢复为默认值,即按标题排序。 我希望实现的是:无论 何时用户返回行程详情视图, 其排序方式都应与之前选择的一致。
为此,我将把 `@State` 改 为 `@ AppStorage`, 并为该设置提供一个唯一标识符。
App Storage 的工作原理 与 `@State` 非常相似。 它声明了一段在 App 中全局有效的状态, 最适合用于简单的可编码类型。
App Storage 的值实际 上会在后台自动存储到磁盘上。 它使用 Apple 平台上的一个名 为 UserDefaults 的 API, 该 API 会为您的 App 存储一组键值对。
这是存储设置、 偏好和 App 配置细节的绝佳场所。
因此,通过将排序偏好保存到 App 存储属性中, 活动页面现在将始终根据用户 在视图中最后选择的选项进行排序, 即使他们在多个不同的行程之间导航也是如此。
最后,今天我想讨论的最后 一个用例是数据持久化。
我刚才展示的那个通过 App 存储保存排序偏好的示例, 实际上就是数据持久化的一个实例。 SwiftUI 会自动使用 UserDefaults API 从磁盘获取该键的值, 然后将视图中的值设置为与之匹配。
如果排序选项被赋予了不同的值, App 存储会将该新值 保存回 UserDefaults。 因此,即使用户在一天后、一周后或一个月后 重新打开你的 App, 排序选项属性也会保持不变,直到被修改为止。
当然,你可能希望持久化的不仅仅是偏好设置。 这款 App 拥有丰富的数据模型, 用户肯定希望将他们的愿望清单保存数天、 数周甚至更长时间。
因此,要在 App 中构建此类 具有持久化功能的数据模型, Swift Data 是一个绝佳的选择。
Swift Data 是一个框架, 它能让你快速为 App 添加持久化功能, 代码量极少且无需外部依赖。
它利用了宏和属性包装器等 现代 Swift 语言特性, 让你只需编写 Swift 代码即可描述模型。
默认情况下,它利用了 Core Data 久经考验的持久化功能。 Swift Data 中的技术 和模型均自动支持可观察性。
要进一步了解 Swift Data, 请观看视频《认识 Swift Data 》和 《使用 Swift Data 构建 App 》。
正如我之前所分享的,数据模型由几个类组成, 其各个属性 以及类之间的关系都作为属性存储 在这些类中。 目前,这些类都使用了 observable 宏进行装饰。
但如果我想使用 Swift Data 实现它们的持久化, 只需将 observable 宏 替换为 model 宏即可。
model 宏会将这些类转换 为 Swift Data 模型。 同样,这也会自动使它们成为可观察对象。
作为一项有趣的练习, 你可以尝试在示例代码发布后下载它, 并按照几个步骤将该 App 迁移 到 Swift Data 上。 对于有兴趣尝试的朋友, 我将解释代码中需要修改的几个关键点。 首先,你需要为模型添加一些元数据。 你需要优化从 App 视图 内部获取或查询数据的方式, 并设置一个容器来存储你的模型。 下面我将快速介绍这些步骤。
首先,将模型从 Observable 切换为 `@Model` 宏后, 你需要遍历各个模型, 并对希望向 Swift Data 提供更多信息的属性添加注解。 例如,在 `trip` 模型中, 你可以在 `activities` 属性上使用 `@Relationship`, 来定义 `trips` 与 `activities` 之间的关系。 这里我将删除规则设置为“级联”, 这样当一个 `trip` 被删除时, 其所有的 `activities` 也会被删除。
我还为 `trip` 属性 设置了反向关系——抱歉, 是 `trip` 属性 在 `activity` 上的反向关系。 这意味着这些活动上的“trip”属性将始终 指向其所属的行程。
现在, Swift Data 让在视图 中获取和显示模型变得极其简单。 通过使用 `@Query`, 你可以直接向 Swift Data 请求一个按 你所需方式排序和过滤的模型数组。
在这个代码片段中,我更新了之前 展示的“近期行程”页面视图。 以前,它需要使用数据源对象来获取 最近添加的行程列表,但借助 Swift Data, 我只需 使用 `@Query`即可。
该查询要求 Swift Data 提供一个按 创建日期降序排序的行程数组。
随后,视图主体在 `for each` 循环中使用了这个最近添加的行程数组。 每当查询结果发生变化(例如 向视图中添加了新行程时), Swift Data 都会更新该视图。
最后,在 App 声明中, 你需要通过添加 ``modelContainer`` 修饰符并传入模型类型的名称, 来配置一个用于存储模型的容器。 App 会为其模型使用默认容器,
因此这三项更改—— 添加 `属性元数据 (Property Metadata)`、 优化查询以及添加 ``modelContainer`` ——是入门的关键。 这便将 Swift Data 强大的持久化功能带入了此类 App 中。
至此,我已详细讲解了“愿望清单”App 的所有数据流用例, 您现在也可以在自己的 SwiftUI App 中采用类似的方法了。 当 App 中的某个视图只需跟踪简单的 UI 状态(例如按钮被点击时), 请使用 `@State`, 然后通过绑定让其他视图或控件共享该状态。
对于使用引用类型(如类)构建的复杂数据模型, 建议考虑使用 `observable` 宏。 当需要保存数据时, 请将数据保存到存储中,以确保其持久存在。 对于设置和偏好等小数据,请使用 App 存储。 而对于模型数据,建议考虑使用 Swift Data 框架。 感谢大家的聆听。 祝大家后续活动愉快。 现在将话筒交还给 Leah。
-