Queryable
是一种类型,它允许你触发视图的呈现,并通过一个 async
函数调用 await
等待其完成,完全隐藏了呈现视图所必需的状态处理。
import SwiftUI
import Queryable
struct ContentView: View {
@StateObject var buttonConfirmation = Queryable<Void, Bool>()
var body: some View {
Button("Commit", action: confirm)
.queryableAlert(controlledBy: buttonConfirmation, title: "Really?") { item, query in
Button("Yes") { query.answer(with: true) }
Button("No") { query.answer(with: false) }
} message: {_ in}
}
@MainActor
private func confirm() {
Task {
do {
let isConfirmed = try await buttonConfirmation.query()
// Do something with the result
} catch {}
}
}
}
这不仅使呈现的视图摆脱了任何上下文(它只是对查询提供一个答案),而且你还可以将 buttonConfirmation
传递到视图层级结构中,以便任何子视图都可以方便地触发确认,而无需处理实际显示的 UI。它适用于 alerts
、confirmationDialogs
、sheets
、fullScreenCover
和完全自定义的 overlays
。
你还可以初始化一个 Queryable
并将其存储在视图模型或使用视图可以访问的任何其他类中。
Queryable 支持 iOS 15+、macOS 12+、watchOS 8+ 和 tvOS 15+。
将以下行添加到你的 Package.swift
文件中的依赖项中
.package(url: "https://github.com/SwiftedMind/Queryable", from: "2.0.0")
前往 File
> Add Packages...
,然后在右上角的搜索字段中输入 URL "https://github.com/SwiftedMind/Queryable"。 Queryable 应该会出现在列表中。选择它,然后单击右下角的 “Add Package”。
要使用,只需在你的代码中导入 Queryable
目标即可。
import SwiftUI
import Queryable
struct ContentView: View {
@StateObject var buttonConfirmation = Queryable<Void, Bool>()
/* ... */
}
你也可以在一个类中定义 Queryable,像这样
@MainActor class MyViewModel: ObservableObject {
let buttonConfirmation = Queryable<Void, Bool>()
}
为了更好地解释 Queryable
的作用,让我们看一个例子。假设我们有一个按钮,其操作需要用户的确认。确认应该以包含两个按钮的警报形式呈现。
通常,你会以类似于以下的方式实现这一点
import SwiftUI
struct ContentView: View {
@State private var isShowingConfirmationAlert = false
var body: some View {
Button("Do it!") {
isShowingConfirmationAlert = true
}
.alert(
"Do you really want to do this?",
isPresented: $isShowingConfirmationAlert
) {
Button("Yes") { confirmAction(true) }
Button("No") { confirmAction(false) }
} message: {}
}
@MainActor private func confirmAction(_ confirmed: Bool) {
print(confirmed)
}
}
代码相当简单。我们每当按下按钮时就会切换警报的呈现,然后使用用户给出的答案调用 confirmAction(_:)
。这种方法没有任何问题,它可以完美地工作。
但是,我相信有一种更方便的方法可以做到这一点。如果你仔细想想,触发警报的呈现并等待某种结果——在本例中是用户的确认——基本上只是一个异步操作。在 Swift 中,有一种机制可以做到这一点:Swift 并发。
如果我们能够简单地 await
等待确认,并将结果作为单个 async
函数调用的返回值,那岂不是很棒?像这样
import SwiftUI
struct ContentView: View {
// Some property that takes care of the view presentation
var buttonConfirmation: /* ?? */
var body: some View {
Button("Do it!") {
confirm()
}
.alert(
"Do you really want to do this?",
isPresented: /* ?? */
) {
Button("Yes") { /* ?? */ }
Button("No") { /* ?? */ }
} message: {}
}
@MainActor private func confirm() {
Task {
do {
// Suspend, show the alert and resume with the user's answer
let isConfirmed = try await buttonConfirmation.query()
} catch {}
}
}
}
其想法是,这个 query()
方法会挂起当前任务,以某种方式切换警报的呈现,然后在我们从未离开作用域的情况下恢复并返回结果。与 UI 的整个用户交互都包含在这一行代码中。
而这正是 Queryable
所做的事情。它是一种类型,你可以使用它从异步上下文中控制视图的呈现。 这是它的样子
import SwiftUI
import Queryable
struct ContentView: View {
// Since we don't need to provide data with the confirmation, we pass `Void` as the Input.
// The Result type should be a Bool.
@StateObject var buttonConfirmation = Queryable<Void, Bool>()
var body: some View {
Button("Commit") {
confirm()
}
.queryableAlert( // Special alert modifier whose presentation is controlled by a Queryable
controlledBy: buttonConfirmation,
title: "Do you really want to do this?"
) { item, query in
// The provided query type lets us return a result
Button("Yes") { query.answer(with: true) }
Button("No") { query.answer(with: false) }
} message: {_ in}
}
@MainActor
private func confirm() {
Task {
do {
let isConfirmed = try await buttonConfirmation.query()
// Do something with the result
} catch {}
}
}
}
在我看来,这看起来和感觉更干净,也更方便。作为一个额外的好处,我们现在可以将警报重用于各种用途,因为它对它的上下文一无所知。
注意
你有责任确保每个查询都在某个时候得到解答(除非取消,请参阅下方)。否则将导致未定义的行为,并可能导致崩溃。这是因为
Queryable
在底层使用了Continuations
。
你可以使用 Queryable
做的另一件有趣的事情是将其传递到视图层级结构中。在以下示例中,MyChildView
不知道来自 ContentView
的警报,但它仍然可以查询确认并接收结果。 如果你稍后在 ContentView
中将 alert
替换为 confirmationDialog
,则 MyChildView
没有任何变化。
import SwiftUI
import Queryable
struct MyChildView: View {
// Passed from a parent view
var buttonConfirmation: Queryable<Void, Bool>
var body: some View {
Button("Confirm Here Instead") {
confirm()
}
}
@MainActor
private func confirm() {
Task {
do {
// This view has no idea how the confirmation is obtained. It doesn't need to!
let isConfirmed = try await buttonConfirmation.query()
// Do something with the result
} catch {}
}
}
}
在上面的例子中,我们使用了 Void
作为 Queryable
的通用 Input
类型,因为确认警报不需要它。 但我们可以传递我们想要的任何值类型。
例如,假设我们想要呈现一个 sheet,用户可以在该 sheet 上创建一个新的 PlayerItem
,然后我们将其保存在数据库中(或发送到后端)。通过使用 PlayerItem
类型的输入进行查询,我们可以为 PlayerEditor
视图提供数据来预填充表单中的一些输入。
struct PlayerItem {
var name: String
/* ... */
static var draft: PlayerItem {/* ... */}
}
struct PlayerListView: View {
@StateObject var playerCreation = Queryable<PlayerItem, PlayerItem>()
var body: some View {
/* ... */
.queryableSheet(controlledBy: playerCreation) { playerDraft, query in
PlayerEditor(draft: playerDraft, onCompletion: { player in
query.answer(with: player)
})
}
}
@MainActor
private func createPlayer() {
Task {
do {
let createdPlayer = try await buttonConfirmation.query(with: PlayerItem.draft)
// Store player in database, for example
} catch {}
}
}
}
这可能非常方便。
可以通过几种方式取消正在进行的查询。
Queryable
属性上调用 cancel()
方法,例如 buttonConfiguration.cancel()
。query()
方法的 Task
被取消。 发生这种情况时,查询将自动被取消并结束视图呈现。Queryable
将检测到这一点并取消任何正在进行的查询。在上述所有情况下,都会抛出一个 QueryCancellationError
。
如果你尝试在另一个查询已经进行时启动一个查询,则会发生冲突。 在这种情况下,默认行为是取消新查询。 你可以通过在初始化期间指定 QueryConflictPolicy
来更改这一点,如下所示
Queryable<Void, Bool>(queryConflictPolicy: .cancelNewQuery)
Queryable<Void, Bool>(queryConflictPolicy: .cancelPreviousQuery)
目前,以下是支持由 Queryable
控制的视图修饰符
queryableAlert(controlledBy:title:actions:message)
queryableConfirmationDialog(controlledBy:title:actions:message)
queryableFullScreenCover(controlledBy:onDismiss:content:)
queryableSheet(controlledBy:onDismiss:content:)
queryableOverlay(controlledBy:animation:alignment:content:)
queryableClosure(controlledBy:block:)
请参阅迁移指南。
MIT 许可证
版权所有 (c) 2023 Dennis Müller 和所有协作者
特此授予任何人获得本软件及其相关文档文件(“软件”)的副本的许可,可以无限制地处理本软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售本软件的副本,并允许向获得本软件的人提供本软件,但须符合以下条件:
上述版权声明和本许可声明应包含在本软件的所有副本或主要部分中。
本软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于适销性、适用于特定用途和不侵权的保证。在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是在合同、侵权行为还是其他方面,由本软件引起、与本软件相关或因使用本软件或其他方式处理本软件而引起。