为你的 iOS 应用实现搜索功能
了解如何使用 SwiftUI 提供的 searchable 修饰器,为你的 App 添加方便好用的搜索功能。
SwiftUI 有一个.searchable
修饰符,可以非常方便的为你的 App 添加高效且易用的搜索功能。
什么是 .searchable
在传统的 UIKit 中,要添加搜索功能,通常需要使用UISearchController
及相关的代理方法,过程较为繁琐。.searchable
是 SwiftUI 在 iOS 15 中引入的一个修饰符,它的目的是让在应用界面中添加搜索功能尽可能的简单:
.searchable 的不同版本
.searchable
有不同的初始化器,适用于不同的 iOS 系统版本。例如,
在 iOS16 上,你可以使用支持 suggestedTokens 参数的 searchable,为搜索功能添加搜索建议:
在 iOS17 上,你可以使用支持 isPresented 参数的 searchable,以便通过编程的方式控制搜索状态:
为 List 组件添加 .searchable
在 List 中使用搜索功能,可能是最常见的使用场景。通过在列表顶部添加搜索栏,用户可以快速过滤列表中的项。
如下示例代码,使用 List 创建了一个列表视图(我省略了一些细节):
struct ContentView: View {
var body: some View {
NavigationStack {
List(filteredRestaurants) { restaurant in
HStack {
Text(restaurant.name)
.font(.headline)
Spacer()
Text(restaurant.priceLevel)
.font(.subheadline)
.foregroundColor(.gray)
}
}
.navigationTitle("上海美食")
}
}
}
要为这个列表添加搜索功能,我们只需要几个简单的步骤:
- 在 List 组件上,添加
.searchable
修饰器 - 创建一个 @State 变量,用于存储用户输入的搜索字符
- 创建一个 filteredRestaurants 变量,它将基于用户的输入来过滤 restaurants。
struct ContentView: View {
@State private var searchText = ""
var filteredRestaurants: [Restaurant] {
if searchText.isEmpty {
return restaurants
} else {
return restaurants.filter { restaurant in
restaurant.name.localizedCaseInsensitiveContains(searchText) ||
restaurant.cuisine.localizedCaseInsensitiveContains(searchText) ||
restaurant.address.localizedCaseInsensitiveContains(searchText)
}
}
}
var body: some View {
NavigationStack {
List(filteredRestaurants) { restaurant in
HStack {
Text(restaurant.name)
.font(.headline)
Spacer()
Text(restaurant.priceLevel)
.font(.subheadline)
.foregroundColor(.gray)
}
}
.navigationTitle("上海美食")
.searchable(
text: $searchText,
prompt: "搜索餐厅名称、菜系或地址"
)
}
}
}
就这么简单。SwiftUI 会自动添加搜索框,为我们处理动画效果等等:
在 iOS 设备上默认隐藏搜索框
在 iOS 设备上,搜索框会默认一直显示:
在一些情况下,比如搜索框的使用频率并不高的时候,你可能并不希望搜索框一直显示,因为它会占据你的屏幕空间。遗憾的是,.searchable
修饰器目前并没有一个原生的 API,用于在 iOS 设备上设置搜索框的可见性。
注意:isPresented
属性用于激活搜索状态,但即使 isPresented
为 false
,搜索框仍然会显示。
为了实现这个需求,我们需要使用一点小技巧,在 View 中条件性地应用 .searchable。也就是说,仅当 showingSearchBar 为 true 时才给列表添加 .searchable 修饰符。
import SwiftUI
struct ContentView: View {
@State private var searchText = ""
@State private var showingSearchBar = false
@State private var restaurants = Restaurant.sampleData
var filteredRestaurants: [Restaurant] {
if searchText.isEmpty {
return restaurants
} else {
return restaurants.filter { restaurant in
restaurant.name.localizedCaseInsensitiveContains(searchText)
|| restaurant.cuisine.localizedCaseInsensitiveContains(
searchText)
|| restaurant.address.localizedCaseInsensitiveContains(
searchText)
}
}
}
var body: some View {
NavigationStack {
List(filteredRestaurants) { restaurant in
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(restaurant.name)
.font(.headline)
Spacer()
Text(restaurant.priceLevel)
.font(.subheadline)
.foregroundColor(.gray)
}
Text(restaurant.cuisine)
.font(.subheadline)
.foregroundColor(.secondary)
Text(restaurant.address)
.font(.caption)
.foregroundColor(.gray)
HStack {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
Text(String(format: "%.1f", restaurant.rating))
.font(.subheadline)
}
}
}
.navigationTitle("上海美食")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
withAnimation(Animation.spring) {
showingSearchBar.toggle()
}
} label: {
Image(systemName: "magnifyingglass")
}
}
}
.onChange(of: showingSearchBar) { _, newValue in
if !newValue {
searchText = ""
}
}
// 当 isSearching 为 true 时才附加 searchable
.if(showingSearchBar) { view in
view.searchable(
text: $searchText,
isPresented: $showingSearchBar,
placement: .navigationBarDrawer(displayMode: .automatic),
prompt: "搜索餐厅名称、菜系或地址"
)
}
}
}
}
extension View {
@ViewBuilder
func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content)
-> some View
{
if condition {
transform(self)
} else {
self
}
}
}
#Preview {
ContentView()
}