大多数浏览器和
Developer App 均支持流媒体播放。
-
为 Swift 和 SwiftUI 带来 Core Data 并发功能
了解 Core Data 如何采用 Swift 5.5 新的并发功能,为您带来更简洁、更高效以及更安全的异步代码。我们将展示如何在您的 app 中更新 Core Data 来处理并发,并详细介绍整个框架中其他许多让 Swift 和 SwiftUI 的使用更具表现力和更强大的改进。
资源
相关视频
WWDC21
-
下载
♪ ♪ 大家好 我是来自Core Data团队的迈克尔勒休 今天我真的很高兴 能和你谈谈团队所做的一些改进 让你在使用Core Data和Swift时 得到相当出色的体验 首先回顾一下 Core Data是一个很好的解决方案 能满足你的客户 在所有Apple平台上的 数据持久化需求 然后我会进一步讨论 Core Data所采用的一些方法 有关Swift运行期中新的 并发的可能性 接下来 我将介绍我们对 Core Data API所做的一些增强 来让你的代码在Swift 中更具表现力 然后通过探索Core Data 向现有SwiftUI的支持 所添加的动态能力来结束讨论 而那正是我们在2020年所介绍过的 不过 让我们从头开始 无论你为哪个Apple 平台开发 你只会有一个应用程序 在某些时候 你的应用程序 最终会遇到用户数据 并且你可能需要将其存储在某个地方 满足这种需求的绝佳选择 是使用Core Data Core Data是Apple的标志性框架 适用于希望持久化的应用程序开发者 以强大且功能丰富的方式 获取用户数据 该框架处理了 合理管理用户数据的诸多复杂性 从它在内存中作为对象图的方式 到它在存储中的建模方式 该框架也花了一番工夫 管理重要的运行注意事项 比如内存使用和延迟 该框架提供的功能也是可扩展的 你可以从简单的本地持久存储开始 并扩展以使用多个执行上下文 来提高性能 甚至通过CloudKit 创建强大的共享数据体验 Core Data也适用于所有 Apple支持的平台 我真的想强调最后一点 一旦你开始使用Core Data 你学到的一切 都将在我们的每个平台上运行 从你的Mac 到你的iPhone 甚至你的Apple Watch 当然 Core Data 在Swift中也能胜任愉快 在过去几年中 我们一直在不断增强核心数据 API 在Swift中尽可能具有表现力 对Swift来说 今年非常激动人心 随着我们 为语言和运行期 引进了全新的并行特性 从开始以来 核心数据就一直在关注并行运行代码 而这是有充分理由的 持久化数据需要 读写一些外部存储媒体 这使得支持新的并行模型成为了一种 再自然不过的选择 让我们在这个“地震”范例应用的 上下文中来探索它是如何工作的 这个应用程序读取了 来自美国地质调查局的数据馈送 并使用Core Data来存储有关 最近地震的信息 例如它们的规模、位置 和事件发生的日期 在架构上 “地震”是一个 Swift应用程序 它有一个视图上下文来驱动UI 和一个背景上下文 用来摄入USGS所提供的数据 我们的范例有一个 为应用程序准备的本地容器 并从USGS的JSON收集地震数据提要
当我们下载数据时 会把它交给我们的JSON解析器 然后将其导入到我们的后台上下文中 转换为托管对象 并保存到我们的本地储存 视图上下文再来会合并更动 神奇地更新我们的用户界面 在2020年 我们专注于如何通过 批处理操作 来高效处理这些数据 但现在 我想聚焦在 如何同时执行这些操作 特别是 我想聚焦在将数据导入应用程序 所要采取的三个步骤 第一步是下载原始数据 当下载成功后 应用程序需要将其转换为 我们特定的本地表示 (representation) 最后 将新对象 保存到持久存储中 我们继续将其转换为高级代码 我已经将每个操作抽象化 变成它自己的函数或闭包 该应用程序首先从服务器 检索原始数据 将其处理为方便的本地表示形式 然后通过在后台的 managedObjectContext 批量插入请求 将对象导入Core Data
像这样写 能更容易 将潜在的瓶颈给具象化 考虑异步执行我们工作 通过网络加载数据将是个绝佳机会 转换也可能是一个值得考虑的地方 此外 将数据导入 我们的持久存储似乎非常合适
但从历史上看 在所有这些情况下 你都必须亲自实施 任何此类的异步机制 或重度依赖 特定于框架的实现 再来说说Core Data的特定抽象
在Core Data的情况下 当你调用了 performAndWait时 托管对象的上下文 将在其自己受保护的执行环境中 执行它所得到的闭包 这可能会占用调用线程 直到工作完成
如果我们将其可视化 我们可以想象三个代码块 我已标记为BEFORE、DURING 和AFTER
当我们的代码运行期 首先标记为BEFORE的代码 有机会在原始线程上执行 然后我们调用performAndWait 而调用线程会阻塞 直到DURING闭包的工作完成 完成该工作后 将执行 叫做AFTER的代码
当然 如果你不需要等待 闭包完成 我们会提供完全非同步的变量 但就在今年一开始 Swift获得了 具有深度语言集成的强大并行模型 它允许Core Data更准确地描述 我们API的意图 语法有点不同 你想要等待表演的结果 但是使用这个新API的心智模型 和managedObject一直支持的 完全一样 然而 好处就是并行 不再是隐藏的实现细节 而是深度整合 进入Swift语言 因为这样 编译器可以自动阻止 许多常见的并行错误 例如数据竞争和死锁 甚至能在已知任务等待结果时 有效利用资源 让我们回到代码 看看实际使用它是什么样子
如我们所见 你要求等待一个 异步宣告的函数 这有可能会 暂停调用执行上下文 直到异步函数通过返回 来让出控制权
它还可以与Swift 现有的结构化错误处理无缝协作 经由将任何抛出的错误 路由到呼叫框架 正如你所期望的那样 现在我们已经看到了一个 调用异步函数的例子 让我们来看看它们是如何宣告的 通过查看Core Data 在managedObject上下文当中 执行异步工作的全新方式 这段Swift代码 包含了相当多的功能 但我只谈一些重要的细节 然后我们就来展示 如何在实际上使用它 从新的执行重载的宣告开始 根据它可以返回的结果类型 你可以看到它是通用的 并用新的关键词async来修饰 它会选择此功能 进入Swift中的新并行功能 也许这个新API最重要的是 它获得的闭包现在允许你 抛出错误或返回一个值 从而节省你手动将这些回传给 呼叫框架的工作 让我们看看这有多酷 来探索几个不同的场景 从历史上看 由于并行隐藏在 我们的实现中 所以在performAndWait之外 传送错误的唯一方法 是关闭一个任选 然后之后再检查一下 如果你执行的是完全异步版本 这可能会更复杂 因为你需要通过传递完成处理程序 来进行很多管道工程 并确保你能够持续使用它们 通过Swift中的新并行模型 所有管道工程都为你处理好了! 只要等待你的异步工作 如果发生错误 就抛出它 事情会自然地展开到调用框架
所以现在 我们聚焦于错误 但结果呢? 好吧 我所描述的一切 作法都完全相同 来看一个具体的例子 在进入代码之前 让我们勾勒一下要做的事情 就这个例子而言 我想配置一个 获取请求来识别过去五小时内 发生的地震次数 用一句话来说 听起来是个简单明了的任务 但是在代码中 我们需要稍微重新排序 我们首先要弄清楚 五小时前是什么时候 我们可以使用日历API 以稳健的方式计算出它 然后 我们将通过谓词 来根据该日期 配置一个获取请求 并要求计数结果类型 在代码中 看起来跟计划相差无几 我们使用日历的补偿API 来计算五小时前的现在 然后通过谓词配置一个 QuakeFetchRequest 来返回一个计数结果 并能对上我们想要的日期 从历史上看 返回结果 遵循着和我们捕捉错误的方式 类似的模式 你会关闭你需要改变的任何状态 在managedObjectContext中 执行你的计算 然后稍后 在重新获得 控制权后使用结果
现在 我们可以等待 我们的perform调用的结果 并将执行结果直接返回到 我们的调用框架 而其余代码则完全相同 我们只是避免通过手动值来运送 以及该代码可能具有的 任何潜在错误或细微差别 这段新代码非常简洁且富有表现力
然而 值得一提的是 有时你应该谨慎行事 来看个不同的例子 就知道为什么了 这个范例尝试将 最近的地震作为托管对象返回 虽然新API让返回值变得非常容易 但当managedObjects已注册为 managedObjectContext时 返回就不安全了 只有在执行调用的闭包内 引用此类注册对象才有效 相反地 如果你需要在不同的 执行上下文之间 引用一个managedObject 要嘛根据需要使用ObjectID 和重新获取 要嘛使用获取请求的 字典表示选项 在我们看另一个例子之前 我想介绍一个还没谈到的细节 这个细节就是 ScheduledTaskType 目前为止 我们看到的每个异步执行 都是针对此选项的默认值: .immediate 还有一个叫做.enqueued的选项 要了解这两种 调度方法之间的区别 想想当你要求安排工作时 在managedObjectContext中 具体发生的事情 会有所帮助 正如我们所见 实时行为很像 这个Swift异步知觉版本的 performAndWait 如果你在不同的执行上下文上运行 并要求等待 在后台上下文上执行的工作 你将会等到它被排程和完成
但是 如果你已经在 相同的执行上下文中 工作将充满希望地立即进入排程
另一方面 队列相对单纯一些 它只会将请求的工作 附加到上下文工作集的末尾 无论发起呼叫站点的关联性如何 让我们继续看另一个例子 你也可以采用 所有这些异步功能 你看 我把我们一直在 谈论的导入逻辑 考虑进去一个新的 importQuakes函数里 且用新的async关键词修饰 反过来讲 这个函数 是根据其他异步功能实现的
现在任何人都可以等待这个新函数 来善用Swift中新的并行功能 来总结一下目前为止我们看到的 总的来说 这个新的API带来了 对Swift结构化并行的支持 直接进入Core Data API执行的新变量 只不过是既有的核心数据API 的Swift并行感知版本 这些都是你们都知道也很喜爱的 我们强烈鼓励在你的 应用程序中利用这个新API
此外 NSManaged ObjectContext并不是 Core Data中唯一支持在其受保护的 并行域中执行任务的类型 我们还向两者添加了类似的API NSPersistentContainer和 NSPersistentStoreCoordinator 这些API的大概性能和行为 与我已经描述的非常相似 但有了全部的并行力量 但若我们没有为你们提供 现有除错工具的随行建议 那就是我的疏忽 当然 XCode提供了 Address和Thread sanitizers 对于抓捕你没有查觉到的错误 非常有帮助 这些都可以在方案编辑器 运行设置的诊断窗格中找到 每个sanitizer检测不同类型的问题 包括验证安全内存使用假设 并适当使用来自多个线程的数据 在向用户社群发布软件之前 使用两种sanitizer 验证你的应用程序 及其相关测试 始终是一个好主意 虽然sanitizer 在所有情况下都很有用 我还是想强调Core Data 提供了一个特殊的运行期旗帜 让你可以启用 以获得更多网域相关的帮助 通过启用此选项 核心数据将会启动数个 有用的断言来验证内部锁 并确认各种Core Data类型使用得当
采用Swift并行支持 并不是今年对Core Data 所做的唯一更改 我们推出的每一个新API 从CloudKit分享 到新的Spotlight整合 全都是基于Swift的思路 来加以呈现 今年 针对这些个别话题 我们有个单独的会议 我鼓励你们去找出来看 我们另外制作了整个框架的通行证 来识别在Swift中 我们还可以做出哪些改进 现在我想展示这其中的一些 那就从我们支持的 各种不同的持久化存储开始讲起 回想一下 持久存储代表着 你实际希望如何存储客户数据 Core Data目前提供了四个 这样的存储: XML、二进制 InMemory和SQLite 你一直都在使用这些标识符 而作为今年的新功能 我们会在 Swift中持续 赋予这些更自然的名称 现有名称会继续运行 但使用这些名称的新API 使用起来更符合人体工学 因为名称较短 还具备自动完成这些符号的能力 当然 持久化存储并不是 Core Data中唯一关注类型的东西 毕竟 框架都跟存储类型化数据有关 而这些类型都是以特性叙述来描述的 在今年 我们在特性描述中 添加了一个新的可扩展枚举 为了与它们的类型运作 提供了更自然的语法 让我们通过编写一个单位测试 来验证我们的运行期模型 是否与我们在XCode模型构建器中 设计的相匹配
为求简单 我们会尝试验证 由地震对象模型所定义的 单一运行期类型 但你可以想象 这会怎么扩展 看起来可能会像个小测试 但验证它是一件好事 因为它在未来可以加快 更有趣的诊断速度 为了编写这个测试 我们将编写一个快速帮助函数 考虑到新的特性类别 现在让我们继续描述这个功能 我们从签名开始 它需要一个特性名称 我们关心的实体描述和类型 且根据新的特性类型枚举进行描述
要定义此实用程序相当简单 首先用提供的名称 验证我们的特性 如果我们找不到它 则测试失败 然后我们来验证 特性的类型属于预期的 全部就是这样 我们可以为每个实体 和属性不断重复 享受我们的运行期行为 与定义的模型相匹配 所带来的安心
这只是今年我们在Swift中 我们对Core Data枚举所做的 一些人体工学改进的范例
直到现在 我一直在专注在 许多较低级别的框架交互上 以及它们如何在Swift中体现 那么 向用户展示数据呢? 在2020年 我们为SwiftUI Core Data的使用上带来了许多便利 现在 我的同事斯科特 想与你分享很多 有关今年推出的新增强功能 斯科特? 谢谢 迈克尔! 在今年使用SwiftUI 和Core Data的体验 可说是进步了不少 从获取请求中的惰性实体解析说起 这放宽了应用程序 在它们构建自己的视图之前 设置其核心数据堆栈的需求 同样在今年 获取请求为了它们的 排序描述符和谓词 改进了动态配置
一种新的获取请求也出现了 而且它支持分段获取 我将通过迈克尔先前提到的 地震范例应用 来一一带你们了解 那就从惰性实体解析开始吧 在你的应用程序中 可能有一些这样的代码 这里的容器属性并不一定 能支持这种类型的代码 甚至更广泛的应用程序 所有这些都是直接从 QuakesProvider类型 来获得它需要的东西 不 这个属性的存在 是为了确保Core Data堆栈 于加载模型之前 在环境中的任何视图 尝试引用任何实体之前 就已经设置好了 看这里 环境视图修饰符被调用了 就在ContentView被初始化之后 在针对今年的SDK进行部署时 就再也不需要这个技巧了 FetchRequest 属性包装器正懒洋洋地 在提取时间内 按照名称来查找实体 此时环境也已经保证了 核心数据堆栈设置好了 所以现在删除这个属性是安全的…
然后再来引用…
QuakesProvider可以 直接在环境调用中共享容器 我们继续讲一些新的API FetchRequest 现在支持动态配置 包装值上有两个新属性 可用于直接更改请求的谓词 以及它的排序描述符 这两者的表达都通过 你所习惯的NSSortDescriptor 以及新的 SortDescriptor值类型 在获取实体时 经由自动生成的托管对象子类 来提供更多便利及安全
最后 有一个配置绑定 与包装值具有相同的属性集 可以更轻松地与视图整合 在这个新API之前 我得设计一下我的视图 这样排序和谓词参数 才能通过视图的初始化程序传递 但也因此变得非常棘手 例如使用工具栏中的控件 配置我的获取请求之类的事情 这些新的动态配置属性 消除了这种阻力 我很乐意向你展示如何通过 对地震范例应用进行添加 分类和过滤 来使用它们 让我们先看看排序描述符 默认情况下 地震应用 会按新近度排序 但我也想按震级排序 所以我要添加一个菜单 让我控制结果的顺序 我首先要添加一个静态的元组数组…
包含我想要支持的排序描述符 以及它们的名称 他们在这里也在使用新的 SortDescriptor类型
我还需要一些状态 来追踪我当前使用的排序顺序 我已经为此创建了一个类型 因此我将其添加为内容视图的属性 现在我要在列表视图中 添加一个工具栏菜单…
这可以修改选定的排序 以及一个onChange修饰符 来更新获取请求的排序描述符
目前在预览中 可以看到新菜单 我可以用它来按震级对地震进行排序 太棒了! 现在添加过滤 我想根据地震的地点设定过滤条件 我要做的第一件事 是搜索框文本的某种状态 我会制作 一个绑定属性…
让搜索框获取请求的更新
有了这些 我需要的就剩用户界面了 方便…
Searchable绑定了一个字符串 所以我们可以把它放在这里
现在在预览中 我们可以缩小到 与三明治匹配地区附近的所有地震 只需在搜寻框中输入“三明治”即可
这就是FetchRequest的动态配置 另一个受到普遍要求的功能 就是分段提取的支持 会在今年作为 新的属性包装器类型出现 这被称为 SectionedFetchRequest 此类型支持与FetchRequest 相同的新动态配置属性 但它使用了附加参数进行初始化 即标明该区段属性的关关键路径 非常类似 NSFetchedResultsController
但与获取结果控件不同的是 标明该区段的属性 可以有任何你喜欢的类型 只要它是可哈希的 这使用了额外的泛型参数 在SectionedFetchRequest上 在类型系统中编码 最后 这种新类型包裹出了 一个二维结果类型 SectionedFetchResults 是区段的集合 每个区段本身都是结果的集合 每区段还各有一个 带有区段标识符的属性
这真的非常容易采用 所以我要将把分段提取 添加到地震应用程序里 首先更新我的FetchRequest宣告
Quake已经有一个用于日期的属性 所以我将把它用于分段关关键路径
接下来 我需要更新主体属性…
来匹配新的分段结果类型
这里的外部循环遍历各个区段 所以我在这里发出一个区段视图 每个区段本身都是地震的集合 所以这个内部ForEach 会对区段进行迭代 就像我之前对结果的迭代一样
如果我们查看预览 我现在得到了 按时间排序并按日分段的地震 SwiftUI甚至为我提供了 对折叠区段的自动支持
这个新的 SectionedFetchRequest类型 支持了与FetchRequest 相同的动态配置属性 以及区段标识符 关键路径的附加配置属性 这超级重要 因为我们实际上 已经不再能安全地更改排序 由于时间和地震震级 没有完美的密切相关 这可能会导致这些区段无法连续 但或许也不是坏事 为了解决这个问题 我需要在脑中更新排序…
所以每个都有一个对应的 区段标识符关关键路径
接下来 往下来到工具栏…
每次我更新排序描述符时 我都要更新区段标识符关关键路径
但这是很重要的部分 每当结果收取器被调用时 对请求的更改都会被提交 所以为了安全地更新排序和区段…
我需要参考我拉进本地端的结果 来更新我的配置
现在在预览中 我们可以看到这改变了顺序 也改变了分区段 我们可以在按时间排序 按日分段的地震 以及按震级排序与分段的 地震之间切换
就是这样:惰性堆栈初始化 动态配置和分段获取 所有这些都可以轻松地 被运用在现有的 iOS 15和MacOS Monterey 的应用程序上 所以回顾一下 Core Data是你的一站式储存 用于管理应用程序的数据持久性需求 并横跨所有Apple平台 它利用了新的并行特性 通过新的执行API 在Swift中可用 并且仍然内建了强大的线程安全除错
它有新的枚举界面 使储存 和特性类别在Swift上 用起来更自然 再加上CloudKit共享和 Spotlight的整合 使用SwiftUI与动态配置 和分段提取来连接视图 变得比以往都更容易
关于这些主题 还有更多新东西 等着我们学习 我们建议收看座谈特集 《使用SwiftUI进行简化 及认识Swift的并行性》 就是这样! 我真的很期待看到 你们会用这些新API构建出什么 [轻快的音乐]
-
-
20:36 - FetchRequest dynamic configuration: sort descriptors
private let sorts = [( name: "Time", descriptors: [SortDescriptor(\Quake.time, order: .reverse)] ), ( name: "Time", descriptors: [SortDescriptor(\Quake.time, order: .forward)] ), ( name: "Magnitude", descriptors: [SortDescriptor(\Quake.magnitude, order: .reverse)] ), ( name: "Magnitude", descriptors: [SortDescriptor(\Quake.magnitude, order: .forward)] )] struct ContentView: View { @FetchRequest(sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)]) private var quakes: FetchedResults<Quake> @State private var selectedSort = SelectedSort() var body: some View { List(quakes) { quake in QuakeRow(quake: quake) } .toolbar { ToolbarItem(placement: .primaryAction) { SortMenu(selection: $selectedSort) .onChange(of: selectedSort) { _ in let sortBy = sorts[selectedSort.index] quakes.sortDescriptors = sortBy.descriptors } } } } struct SelectedSort: Equatable { var by = 0 var order = 0 var index: Int { by + order } } struct SortMenu: View { @Binding private var selectedSort: SelectedSort init(selection: Binding<SelectedSort>) { _selectedSort = selection } var body: some View { Menu { Picker("Sort By", selection: $selectedSort.by) { ForEach(Array(stride(from: 0, to: sorts.count, by: 2)), id: \.self) { index in Text(sorts[index].name).tag(index) } } Picker("Sort Order", selection: $selectedSort.order) { let sortBy = sorts[selectedSort.by + selectedSort.order] let sortOrders = sortOrders(for: sortBy.name) ForEach(0..<sortOrders.count, id: \.self) { index in Text(sortOrders[index]).tag(index) } } } label: { Label("More", systemImage: "ellipsis.circle") } .pickerStyle(InlinePickerStyle()) } private func sortOrders(for name: String) -> [String] { switch name { case "Magnitude": return ["Highest to Lowest", "Lowest to Highest"] case "Time": return ["Newest on Top", "Oldest on Top"] default: return [] } } } }
-
21:33 - FetchRequest dynamic configuration: predicates
struct ContentView: View { @FetchRequest(sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)]) private var quakes: FetchedResults<Quake> @State private var searchText = "" var query: Binding<String> { Binding { searchText } set: { newValue in searchText = newValue quakes.nsPredicate = newValue.isEmpty ? nil : NSPredicate(format: "place CONTAINS %@", newValue) } } var body: some View { List(quakes) { quake in QuakeRow(quake: quake) } .searchable(text: query) } }
-
23:26 - SectionedFetchRequest
extension Quake { lazy var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "MMMM d, yyyy" return formatter }() @objc var day: String { return dateFormatter.string(from: time) } } struct ContentView: View { @SectionedFetchRequest( sectionIdentifier: \.day, sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)]) private var quakes: SectionedFetchResults<String, Quake> var body: some View { List { ForEach(quakes) { section in Section(header: Text(section.id)) { ForEach(section) { quake in QuakeRow(quake: quake) } } } } } }
-
24:56 - SectionedFetchRequest dynamic configuration: sort descriptors
extension Quake { lazy var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "MMMM d, yyyy" return formatter }() @objc var day: String { return dateFormatter.string(from: time) } @objc var magnitude_str: String { return "\(magnitude)" } } private let sorts = [( name: "Time", descriptors: [SortDescriptor(\Quake.time, order: .reverse)], section: \Quake.day ), ( name: "Time", descriptors: [SortDescriptor(\Quake.time, order: .forward)], section: \Quake.day ), ( name: "Magnitude", descriptors: [SortDescriptor(\Quake.magnitude, order: .reverse)], section: \Quake.magnitude_str ), ( name: "Magnitude", descriptors: [SortDescriptor(\Quake.magnitude, order: .forward)], section: \Quake.magnitude_str )] struct ContentView: View { @SectionedFetchRequest( sectionIdentifier: \.day, sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)]) private var quakes: SectionedFetchResults<String, Quake> @State private var selectedSort = SelectedSort() var body: some View { List { ForEach(quakes) { section in Section(header: Text(section.id)) { ForEach(section) { quake in QuakeRow(quake: quake) } } } } .toolbar { ToolbarItem(placement: .primaryAction) { SortMenu(selection: $selectedSort) .onChange(of: selectedSort) { _ in let sortBy = sorts[selectedSort.index] let config = quakes config.sectionIdentifier = sortBy.section config.sortDescriptors = sortBy.descriptors } } } } struct SelectedSort: Equatable { var by = 0 var order = 0 var index: Int { by + order } } struct SortMenu: View { @Binding private var selectedSort: SelectedSort init(selection: Binding<SelectedSort>) { _selectedSort = selection } var body: some View { Menu { Picker("Sort By", selection: $selectedSort.by) { ForEach(Array(stride(from: 0, to: sorts.count, by: 2)), id: \.self) { index in Text(sorts[index].name).tag(index) } } Picker("Sort Order", selection: $selectedSort.order) { let sortBy = sorts[selectedSort.by + selectedSort.order] let sortOrders = sortOrders(for: sortBy.name) ForEach(0..<sortOrders.count, id: \.self) { index in Text(sortOrders[index]).tag(index) } } } label: { Label("More", systemImage: "ellipsis.circle") } .pickerStyle(InlinePickerStyle()) } private func sortOrders(for name: String) -> [String] { switch name { case "Magnitude": return ["Highest to Lowest", "Lowest to Highest"] case "Time": return ["Newest on Top", "Oldest on Top"] default: return [] } } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。