9.2 执行搜索操作

原文链接: https://developer.apple.com/documentation/swiftui/performing-a-search-operation

9.2 执行搜索操作

根据你存储的搜索文本以及可选令牌更新搜索结果。

概述

要在应用的数据模型中执行搜索,先为查询文本创建存储,并用 searchable 视图修饰符呈现它。由于存储由你管理,你可以检测到它的变化并相应地更新搜索操作。随着人们输入而更新搜索结果,可以确保应用的搜索界面响应及时。

你也可以选择为令牌提供存储,令牌是你的应用能够识别的离散搜索词。令牌提供了一种组合多个搜索词的方式,也让你更容易表明某个搜索词在你的应用中很常见或在意料之中。

一个圆角宽矩形,左边是放大镜,右边是 Apple、Banana 和 Pear 三个词,每个词各自位于一个深灰色矩形中。矩形最右侧有一个深灰色圆形里的 X。

关于如何控制搜索框在应用界面中的位置,参见为应用添加搜索界面。

为字符串提供存储

searchable 修饰符接收一个指向字符串值的 Binding,作为 text 输入。这个字符串充当 SwiftUI 所显示的搜索查询框的存储。你可以在视图内用 State 属性创建这份存储,并把它初始化为空字符串:

1
@State private var searchText: String = ""

为了更容易在不同视图之间共享搜索查询,你可以在属于应用模型的可观察对象中创建一个已发布值:

1
2
3
class Model: ObservableObject {
    @Published var searchText: String = ""
}

无论哪种方式,都要给这个字符串加上美元符号($)前缀,把它的 Binding 传入 searchable 视图修饰符:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct ContentView: View {
    @EnvironmentObject private var model: Model
    @State private var departmentId: Department.ID?
    @State private var productId: Product.ID?

    var body: some View {
        NavigationSplitView {
            DepartmentList(departmentId: $departmentId)
        } content: {
            ProductList(departmentId: departmentId, productId: $productId)
                .searchable(text: $model.searchText)
        } detail: {
            ProductDetails(productId: productId)
        }
    }
}

为令牌提供存储

除了搜索字符串之外,当你使用带 tokens 参数的 searchable 修饰符(例如 searchable(text:tokens:placement:prompt:token:))时,搜索框还可以显示令牌。

创建令牌的方式是:先定义一组遵循 Identifiable 协议的值,然后实例化该值的集合。例如,你可以创建一个水果令牌的枚举:

1
2
3
4
5
6
enum FruitToken: String, Identifiable, Hashable, CaseIterable {
    case apple
    case pear
    case banana
    var id: Self { self }
}

然后给模型添加一个新的已发布属性,用来存储一组令牌:

1
@Published var tokens: [FruitToken] = []

要显示令牌,把一个指向 tokens 数组的 Binding 作为 searchable 修饰符的 tokens 输入参数传入,并用 token 闭包描述如何绘制每个令牌。在该闭包中,返回表示作为输入传入的那个令牌的 View。例如,你可以用 Text 视图表示每个令牌:

1
2
3
4
5
6
7
8
ProductList(departmentId: departmentId, productId: $productId)
    .searchable(text: $model.searchText, tokens: $model.tokens) { token in
        switch token {
        case .apple: Text("Apple")
        case .pear: Text("Pear")
        case .banana: Text("Banana")
        }
    }

如上面的例子所示,你可以用 Text 视图表示令牌。在 iOS 和 iPadOS 上,你也可以改用 Label。请确保该视图清楚地表示对应的搜索查询;如果使用标签,还要确保令牌的高度与搜索查询框相匹配。令牌出现在搜索框中所有纯文本之前。下图展示当 tokens 数组包含 apple 和 banana 两个令牌时搜索框的样子:

一个带三个导航窗格的 macOS 窗口。左侧窗格列出 Produce、Frozen 和 Bakery 三项。中间窗格显示占位文本 Select a Department。右侧窗格显示占位文本 Select a Product。工具栏在窗口右上角有一个搜索框,里面有 Apple 和 Banana 两个词,各自位于一个灰色矩形中。

支持含可变部分的令牌

你可以在 token 闭包中使用 Picker,让人们修改代表令牌的部分数据。例如,假设你的水果令牌数据同时包含种类和水分状态两个属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct FruitToken: String, Identifiable, Hashable, CaseIterable {
    enum Kind {
        case apple
        case pear
        case banana
        var id: Self { self }
    }

    enum Hydration: String, Identifiable, Hashable, CaseIterable {
        case hydrated
        case dehydrated
    }

    var kind: Kind
    var hydration: Hydration = .hydrated
}

针对这个新模型,在 token 闭包中加上美元符号($)来指定指向该令牌的 Binding。用这个绑定为 hydration 属性创建选择器,并用 kind 属性作为标签:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
ProductList(departmentId: departmentId, productId: $productId)
    .searchable(text: $model.searchText, tokens: $model.tokens) { $token in
        Picker(selection: $token.hydration) {
            ForEach(FruitToken.Hydration.allCases) { hydration in
                switch hydration {
                case .hydrated: Text("Hydrated")
                case .dehydrated: Text("Dehydrated")
                }
            }
        } label: {
            switch token.kind {
            case .apple: Text("Apple")
            case .pear: Text("Pear")
            case .banana: Text("Banana")
            }
        }
    }

为人们提供向搜索框添加令牌的方式。你可以用不同的做法,例如:

  • 使用带 suggestedTokens 输入参数的某个 searchable 视图修饰符(例如 searchable(text:tokens:suggestedTokens:placement:prompt:token:))来建议要加入数组的令牌。人们从搜索框下方出现的列表中选择建议。关于为令牌以及搜索文本提供建议的更多信息,参见提供搜索词建议。
  • 在人们编辑搜索字符串时监听它。当你检测到与某个令牌匹配的子串时,从字符串中移除相关字符、把文本转换为令牌,并把对应的令牌加入 tokens 数组。
  • 等到在搜索字符串中看到逗号或空格这类分隔字符,再尝试把前面的字符切分成令牌。
  • 等到有人提交搜索,再尝试把整个搜索字符串切分成令牌。关于提交搜索的更多信息,参见管理搜索界面的激活。

当你检测到搜索查询发生变化时,应用就可以开始搜索。搜索操作如何执行取决于你的应用如何存储和呈现数据。一种做法是:根据列表条目中的某个字段是否与搜索查询匹配,过滤 List 中显示的元素。例如,你可以写一个方法,只返回商品数组中名称与搜索文本或当前搜索框中某个令牌相匹配的条目:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func filteredProducts(
    products: [Product],
    searchText: String,
    tokens: [FruitToken]
) -> [Product] {
    guard !searchText.isEmpty || !tokens.isEmpty else { return products }
    return products.filter { product in
        product.name.lowercased().contains(searchText.lowercased()) ||
        tokens.map({ $0.rawValue }).contains(product.name.lowercased())
    }
}

请考虑搜索的复杂度和改变搜索词的代价。如果代价很高——例如更新需要访问网络,或者过滤逻辑很复杂——可以考虑预取并缓存数据,或者降低更新频率。你也可以等到有人提交查询之后再执行搜索。关于检测查询提交的信息,参见管理搜索界面的激活。

如果搜索空间可以划分为宽泛的类别,你可以提供作用域,帮助人们更快地缩小搜索范围。参见限定搜索操作的范围。