大多数浏览器和
Developer App 均支持流媒体播放。
-
SwiftUI 中的 App 要领
借助新的 App 协议,现在 SwiftUI 已支持构建完整的 app 了。了解 App、Scene 和 View 如何一起配合。并了解在节省时间和降低复杂性的同时,您如何轻松实现用户期望能从一流产品中获得的功能。使用新的命令修饰符轻松地向界面中添加预期功能,并探索新的 WindowGroup API 的详情。 为能充分利用本节内容,你应该先对 SwiftUI 有一定的了解。请观看“SwiftUI 介绍”以获取入门知识。 如需更多 SwiftUI 相关信息,可选择:“SwiftUI 新增功能”,“Swift UI 中的数据要素”,“SwiftUI 中的叠放、网格和轮廓”以及“在 SwiftUI 中构建基于文档的 app”。
资源
相关视频
WWDC20
-
下载
(你好 WWDC 2020)
大家好 欢迎来到 WWDC
欢迎来到“SwiftUI 中的 app 要领” 我叫 Matt Ricketson 在 SwiftUI 工作 稍后 我的同事 Jeff 也将与大家见面 去年我们推出了 SwiftUI 在 Apple 的所有平台上 构建优秀用户界面的强大新方法 我们使用视图构建用户界面 SwiftUI 提供了一套用于修改视图的 API 并把它们组合在一起
今年 我们将扩展该框架 使用新的 API 来声明场景和 app 对使用 SwiftUI 可以构建的内容 以充分扩展 最重要的是 你现在只用 SwiftUI 就可以构建一个完整的 app 在本节中 我们将介绍这些新的 API 并解释视图、场景和 app 如何协同工作
接下来 我们将深入探讨 SwiftUI 的场景架构 并展示如何在 app 中自定义场景 最后 我们将简要介绍一下 可用于自定义 app 的不同 API 以及可以去哪里了解更多信息 现在 让我们从讨论视图开始 如果你以前使用过 SwiftUI 那么你就已经很熟悉视图了 视图很重要 因为每个视图都定义了 用户界面中的内容 在看到某个 app 时 你看到的一切都是视图 单独的图像和文本片段都是视图 容纳它们的容器也是视图 事实上 你在屏幕上看到的每一个像素 都是由视图定义的
但并不是所有的视图都属于同一个 app 因为 app 不能完全控制整个屏幕 相反 平台控制 app 的呈现方式 显示不同区域的 app 片段
在 SwiftUI 中 我们将这些 截然不同的区域称为“场景”
窗口是在屏幕上显示场景内容的 最常见方式 有些平台 比如 iPadOS 可以并排显示多个窗口 其他平台 如 iOS watchOS 和 tvOS 则倾向于每个 app 只显示一个 单一的全屏幕窗口 macOS 是另一个很好的例子 可看到场景内容如何以不同方式出现 在本例中 我们看到相关窗口的集合 其中每个窗口都是不同场景内容的 表现形式
macOS 还允许你将相关窗口收集到 单个选项卡式窗口中 在本例中 我们的场景被表示为单独的选项卡
这个共享窗口也由它自己的场景表示 作为与每个选项卡相关联的子场景容器
这些场景集合构成了 某款 app 的全部内容 app、场景和视图共同构成了 统一的所有制层次结构 (APP - 场景 - 视图) 正如我们之前提到的 视图作为基本的构建块 呈现屏幕上看到的所有内容 并且可以组合在一起 形成更复杂的用户界面
视图构成了场景的内容 让其由平台独立展示
与视图一样 这些场景也可以组合在一起 形成更复杂的场景 就像我们前面看到的选项卡式 窗口示例一样
而最后 所有这些场景构成了App 的内容
既然我们已经了解 app、场景 和视图是如何协同工作的 那么让我们看看这是 如何在 SwiftUI 代码中实现的
这里可以看到我在 SwiftUI 写的 一个基础款 app 可跟踪我在读书俱乐部里所阅读的书 正如你所看到的 SwiftUI 中的 app 有一个简明声明 意思是像这样的基础款 app 只需要几行代码即可 没有了多余的样板文件 你就可以立即将注意力集中在 app 特有的代码上
在本例中 我们使用名为 ReadingListViewer 的视图 定义了 app 的实际界面
ReadingListViewer 是我单独构建的一个自定义视图 可以让我浏览我的阅读清单
ReadingListViewer 包含在 一个名为 WindowGroup 的场景中
WindowGroup 场景管理 ReadingListViewer 将呈现的窗口
它还可以在支持这些功能的平台上 创建其他窗口 或同一窗口中的新选项卡 Jeff 将在稍后的谈话中更详细地解释 WindowGroup 是如何工作的
我们的 WindowGroup 场景 包含在一个 app 中 由符合 App 协议的自定义结构表示
你会注意到代码的结构 与我们前面看到的所有权层次结构相匹配 app 包含场景 场景包含视图
你可能还注意到 我们的 app 声明 看起来类似于自定义视图声明 例如 视图和 app 都能够 声明数据依赖关系 这里 我们的 ReadingListViewer 观察一个 ReadingListStore 对象
我们的 app 还依赖于 一个 ReadingListStore 对象 却使用 StateObject 属性包装 声明自己为该对象的所有者 这是今年 SwiftUI 的一个新特性
视图和 app 都声明了一个“body”属性 用来定义它们的用户界面内容
我们前面讨论了视图 是如何由其他视图组成的 这就是视图的主体要返回视图的原因 然而 app 是使用场景构建的 因此它的 body 属性返回一个场景
最后 你可能注意到了修饰 app 的 @main 属性 这是 Swift 5.3 中的新属性 允许类型用作程序执行的 入口点
通常 Swift 计划需要 一个 main.Swift 文件才能执行 使用 @main 属性 我们可以将这个责任委托给 app 结构体 在启动时自动执行所有必要的设置 让我们的 app 出现在屏幕上
现在我们已经回顾了代码 再回去看一下
我们在这里看到的是 SwiftUI 中 一个基础款 app 的完整声明 只需要几行代码 但别让这个蒙蔽了你 有很多自动的、智能的行为 包含在这份简单的声明里
要真正了解这个 app 的工作原理 我们需要更多地谈谈 WindowGroup 场景 它管理我们的用户界面 为此 我要让 Jeff 继续讨论这个话题 谢了 Matt 大家好
首先 我想通过一个简短的演示 让你看看 Matt 在实践中提出的 一些概念 然后我将讨论 WindowGroup 的一些细节
我是一个非常狂热的读者 所以我一直在开发一个小 app 来追踪我在当前阅读的所有书籍的进度 正如你所见 我的 app 在 iPadOS 上 启动时有一个初始窗口 使用我指定为内容的视图 看起来我在读好几本书 接着来打开一些新窗口 这样我就可以检查我的进度了 如果我打开 App Exposé 就很容易地在这里创建一个新窗口 然后浏览到另一本书 WindowGroup 在 iPadOS 上 自动为我的 app 提供此功能 你会注意到我的每个窗口 都反映了界面中不同的状态 所选的书各不相同 而更改其中一个并不会影响其他的 这是 SwiftUI 中场景的一个关键方面 app 可以为每个场景提供一个共享模型 但这些场景中的视图状态将是独立的 我还想在 App 切换器中指出一些注意点 我的每个窗口都显示 app 的名称 以及所选书籍的名称 这是通过我们今年推出的 一个新的视图修饰符完成的 称之为“浏览标题” 在 iOS 上可用于填充浏览栏 和 App 切换器中的标题 这是视图修饰符的一个示例 可以影响其父场景的状态
在 Mac上 app 支持多个窗口是很常见的 通过在 app 中使用 WindowGroup SwiftUI 将在文件菜单中提供一个菜单项 该菜单栏支持创建新的场景实例
也可以通过 标准的 Command-N 键盘快捷键调用此项
你将注意到浏览标题在 macOS 上 是如何应用的 书名显示在 窗口的标题栏区域
它也将在窗口菜单中使用
在这里提供一个好标题对用户来说 是很重要的 因为除了为他们的交互提供 更多的环境之外 还可以帮助他们从打开的窗口列表中 选择他们想要的窗口 除了支持多个窗口之外 macOS 还支持将其窗口分组在一起 通过窗口菜单 我可以将打开的窗口合并到 一个单一选项卡式界面中
每个选项卡由单独的场景表示
同样 我不必为这个函数编写任何代码 SwiftUI 自动提供此函数
现在你已经了解了一些在 app 中 使用 WindowGroup 场景的实际含义 那么让我们再来讨论一些细节 重述一下我刚才给你们看的内容 WindowGroup 是一个场景 它允许你表达 app 的主界面
将使用你提供给它的视图 作为该接口的定义
这在我们所有的平台上 都能以预期的方式工作 例如 在 iOS 和 watchOS 上 你的视图将显示在一个 占据设备整个屏幕的窗口中 在 macOS 上 此窗口将根据视图的定义调整大小 场景的生命周期 由运行它们的平台管理
以 macOS 为例 当平台需要为你的 app 创建窗口时 WindowGroup 将实例化一个新的子场景 默认情况下 它将在窗口中显示其内容
在支持多个窗口的平台上 例如 macOS 和 iPadOS WindowGroup 可以实例化多个子窗口
这可以在响应用户操作时发生 例如点击菜单项或调用多任务手势
虽然每个场景共享其用户界面的定义 但组成该定义的视图 都有自己独立的状态
这意味着更改一个窗口中的视图状态 不会影响其他窗口的状态
此功能允许你提供 用于界面的模板 同时允许用户通过你提供的状态 自定义此界面
由于平台掌管着场景的生命周期 我们今年推出了一个新的属性包装器 以帮助你管理视图状态的还原 SceneStorage 属性包装可用于 持久化视图状态
它需要一个唯一密钥来标识要存储的状态 然后 SwiftUI 将自动保存 并在适当时间恢复该状态 (自定义 App) 现在我已经向你展示了更多关于场景 特别是 WindowGroup 的工作原理 我想交给 Matt 来继续讨论 他会给你更多关于 如何进一步自定义 app 的信息 谢了 Jeff 在我们结束之前 我想让你看看 今年新推出的一些与 app 相关的特性
我们之前看到的 BookClub app 是一个数据驱动的 app 由共享数据模型支持 但也有其他类型的 app 例如基于文档的 app 就像我们在这里看到的 ShapeEdit app 一样
今年新增的是 DocumentGroup 场景类型 它自动管理基于文档的场景的 打开、编辑和保存
若要了解更多信息 请查看 今年的《在 SwiftUI 中构建基于文档 的 app 》讲座
现在 回到我们的 BookClub 示例 macOS app 的一个常见特性 是首选项窗口
今年我们公开了新的“设置”场景类型 可在 macOS 上使用 它会自动设置标准首选项窗口 设置场景将自动设置在 app 菜单中的 首选项命令 并且还给予窗口正确的样式处理
说到菜单命令 SwiftUI 还提供了一个 API 让你使用新的“commands”修饰符 向场景添加自定义命令
BookCommands 是我定义的自定义类型 我们快来看看吧
commands API 功能强大且灵活 我们在视图、场景和 app 中使用的 相同的声明式 状态驱动的编程模型 你可以将命令封装在自定义类型中 根据用户关注点确定操作目标 类似于 AppKit 或 UIKit 中的 响应程序链 并使用普通视图构建命令本身 要想更深入了解 如何使用命令的内容 请查看我们的参考文档 这只是 SwiftUI 今年新推出的 app 相关 API 的简单介绍 我建议你也去看看 SwiftUI 的其他讲座 有助于让你构建 app 的内容
《 SwiftUI 中的数据要素》可助你了解 如何在 app、场景和视图之间 正确地传递数据 而《 Swift 的新功能》将向你展示 语言的最新改进 可帮助改进所有的 SwiftUI 代码
我们真的很期待看到你所构建的 所有优秀的 SwiftUI app 我们希望你在论坛上与我们分享你的创作 我们期待你所有的反馈 并迫不及待地 想看看你接下来要做什么 谢谢 祝你 WWDC 参会愉快
-
-
3:57 - Book club app
@main struct BookClubApp: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { NavigationView { List(store.books) { book in Text(book.title) } .navigationTitle("Currently Reading") } } } class ReadingListStore: ObservableObject { init() {} var books = [ Book(title: "Book #1", author: "Author #1"), Book(title: "Book #2", author: "Author #2"), Book(title: "Book #3", author: "Author #3") ] } struct Book: Identifiable { let id = UUID() let title: String let author: String }
-
10:21 - Window groups
@main struct BookClubApp: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { NavigationView { List(store.books) { book in Text(book.title) } .navigationTitle("Currently Reading") } } } class ReadingListStore: ObservableObject { init() {} var books = [ Book(title: "Book #1", author: "Author #1"), Book(title: "Book #2", author: "Author #2"), Book(title: "Book #3", author: "Author #3") ] } struct Book: Identifiable { let id = UUID() let title: String let author: String }
-
12:07 - Scene storage
@main struct BookClubApp: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore @SceneStorage("selectedItem") private var selectedItem: String? var selectedID: Binding<UUID?> { Binding<UUID?>( get: { selectedItem.flatMap { UUID(uuidString: $0) } }, set: { selectedItem = $0?.uuidString } ) } var body: some View { NavigationView { List(store.books) { book in NavigationLink( destination: Text(book.title), tag: book.id, selection: selectedID ) { Text(book.title) } } .navigationTitle("Currently Reading") } } } class ReadingListStore: ObservableObject { init() {} var books = [ Book(title: "Book #1", author: "Author #1"), Book(title: "Book #2", author: "Author #2"), Book(title: "Book #3", author: "Author #3") ] } struct Book: Identifiable { let id = UUID() let title: String let author: String }
-
12:59 - Document groups
import SwiftUI import UniformTypeIdentifiers @main struct ShapeEditApp: App { var body: some Scene { DocumentGroup(newDocument: ShapeDocument()) { file in DocumentView(document: file.$document) } } } struct DocumentView: View { @Binding var document: ShapeDocument var body: some View { Text(document.title) .frame(width: 300, height: 200) } } struct ShapeDocument: Codable { var title: String = "Untitled" } extension UTType { static let shapeEditDocument = UTType(exportedAs: "com.example.ShapeEdit.shapes") } extension ShapeDocument: FileDocument { static var readableContentTypes: [UTType] { [.shapeEditDocument] } init(fileWrapper: FileWrapper, contentType: UTType) throws { let data = fileWrapper.regularFileContents! self = try JSONDecoder().decode(Self.self, from: data) } func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws { let data = try JSONEncoder().encode(self) fileWrapper = FileWrapper(regularFileWithContents: data) } }
-
13:27 - Settings scene
@main struct BookClubApp: App { @StateObject private var store = ReadingListStore() @SceneBuilder var body: some Scene { WindowGroup { ReadingListViewer(store: store) } #if os(macOS) Settings { BookClubSettingsView() } #endif } } struct BookClubSettingsView: View { var body: some View { Text("Add your settings UI here.") .padding() } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { NavigationView { List(store.books) { book in Text(book.title) } .navigationTitle("Currently Reading") } } } class ReadingListStore: ObservableObject { init() {} var books = [ Book2(title: "Book #1", author: "Author #1"), Book2(title: "Book #2", author: "Author #2"), Book2(title: "Book #3", author: "Author #3") ] } struct Book: Identifiable { let id = UUID() let title: String let author: String }
-
14:07 - BookCommands
struct BookCommands: Commands { @FocusedBinding(\.selectedBook) private var selectedBook: Book? var body: some Commands { CommandMenu("Book") { Section { Button("Update Progress...", action: updateProgress) .keyboardShortcut("u") Button("Mark Completed", action: markCompleted) .keyboardShortcut("C") } .disabled(selectedBook == nil) } } private func updateProgress() { selectedBook?.updateProgress() } private func markCompleted() { selectedBook?.markCompleted() } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。