Queryable

GitHub release (latest by date) GitHub

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。它适用于 alertsconfirmationDialogssheetsfullScreenCover 和完全自定义的 overlays

你还可以初始化一个 Queryable 并将其存储在视图模型或使用视图可以访问的任何其他类中。

安装

Queryable 支持 iOS 15+、macOS 12+、watchOS 8+ 和 tvOS 15+。

在 Swift Package 中

将以下行添加到你的 Package.swift 文件中的依赖项中

.package(url: "https://github.com/SwiftedMind/Queryable", from: "2.0.0")

在 Xcode 项目中

前往 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 {}
    }
  }
}

这可能非常方便。

取消查询

可以通过几种方式取消正在进行的查询。

在上述所有情况下,都会抛出一个 QueryCancellationError

处理冲突

如果你尝试在另一个查询已经进行时启动一个查询,则会发生冲突。 在这种情况下,默认行为是取消新查询。 你可以通过在初始化期间指定 QueryConflictPolicy 来更改这一点,如下所示

Queryable<Void, Bool>(queryConflictPolicy: .cancelNewQuery)
Queryable<Void, Bool>(queryConflictPolicy: .cancelPreviousQuery)

支持的 Queryable 修饰符

目前,以下是支持由 Queryable 控制的视图修饰符

更新到 Queryable 2.0.0

请参阅迁移指南

许可证

MIT 许可证

版权所有 (c) 2023 Dennis Müller 和所有协作者

特此授予任何人获得本软件及其相关文档文件(“软件”)的副本的许可,可以无限制地处理本软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售本软件的副本,并允许向获得本软件的人提供本软件,但须符合以下条件:

上述版权声明和本许可声明应包含在本软件的所有副本或主要部分中。

本软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于适销性、适用于特定用途和不侵权的保证。在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是在合同、侵权行为还是其他方面,由本软件引起、与本软件相关或因使用本软件或其他方式处理本软件而引起。