SwiftUI原生SearchBar:文本变化触发API请求实现方案问询
SwiftUI 原生搜索实现与API请求触发方案
一、纯SwiftUI原生实现的可行性
完全可以!iOS 15及以后的SwiftUI提供的searchable修饰符已经覆盖了你之前用UIKit封装UISearchBar的所有核心功能:
- 自动显示搜索框与取消按钮,点击取消按钮会自动清空文本并退出编辑状态
- 支持绑定搜索文本、设置提示语
- 内置搜索建议展示能力(就是你代码里注释掉的部分)
不需要再依赖UIViewRepresentable套UIKit的UISearchBar,纯SwiftUI就能搞定。
二、搜索文本变化时触发API请求的实现方案
核心思路是监听搜索文本的变化,结合防抖逻辑避免频繁请求,同时在ViewModel中封装API调用逻辑。以下是针对你的代码修改后的完整实现:
1. 补充ViewModel的搜索逻辑
import Foundation class SubListViewModel: ObservableObject { @Published var searchResults: [Movie] = [] @Published var isLoading = false // 搜索API调用方法,支持分页 func searchMovies(query: String, page: Int = 1) async { guard !query.isEmpty else { searchResults = [] return } isLoading = true defer { isLoading = false } // 替换成你实际的API请求逻辑 do { let results = try await MovieAPI.search(query: query, page: page) // 第一页替换结果,后续页追加结果 if page == 1 { searchResults = results } else { searchResults.append(contentsOf: results) } } catch { print("搜索失败:\(error.localizedDescription)") // 这里可以添加错误提示逻辑 } } } // 示例模型,根据你的实际API返回调整 struct Movie: Identifiable, Equatable { let id = UUID() let title: String let language: String let year: String let imgUrl: String } // 示例API服务,替换成你的实际API调用 enum MovieAPI { static func search(query: String, page: Int) async throws -> [Movie] { // 模拟网络延迟 try await Task.sleep(nanoseconds: 500_000_000) return (1...5).map { Movie(title: "\(query) 作品\($0)", language: "中文", year: "202\($0)", imgUrl: "https://example.com/img\($0).jpg") } } }
2. 修改MainScreenView实现搜索与分页
import SwiftUI struct MainScreenView: View { @StateObject var subListViewModel = SubListViewModel() @State private var searchText = "" @State private var page = 1 // 用于防抖的任务引用,取消旧的请求 @State private var searchTask: Task<Void, Never>? var body: some View { TabView { NavigationStack { List { ForEach(subListViewModel.searchResults) { sub in VStack(alignment: .leading, spacing: 8) { Text(sub.title) .font(.headline) Text("\(sub.year) · \(sub.language)") .font(.subheadline) .foregroundColor(.secondary) // 异步加载图片 AsyncImage(url: URL(string: sub.imgUrl)) { image in image.resizable().scaledToFit() } placeholder: { ProgressView() } .frame(height: 150) } .padding(.vertical, 4) } // 加载更多的提示 if subListViewModel.isLoading { ProgressView() .frame(maxWidth: .infinity) } } .navigationTitle("搜索") // 监听滚动到底部,加载下一页 .safeAreaInset(edge: .bottom) { Color.clear.frame(height: 10) } .onScrollVisibilityChange(for: .bottom) { visibility in if visibility.isVisible, !subListViewModel.isLoading, !searchText.isEmpty { Task { await subListViewModel.searchMovies(query: searchText, page: page + 1) page += 1 } } } } .searchable(text: $searchText, prompt: "输入电影/剧集名称") { // 展示搜索建议 ForEach(subListViewModel.searchResults) { result in Text("你要找的是\(result.title)吗?") .searchCompletion(result.title) } } // 监听搜索文本变化,触发防抖请求 .onChange(of: searchText) { newText in searchTask?.cancel() searchTask = Task { // 0.5秒防抖,避免输入过程中频繁调用API try? await Task.sleep(nanoseconds: 500_000_000) // 确保任务未取消,且当前文本和触发时一致 guard !Task.isCancelled, searchText == newText else { return } await subListViewModel.searchMovies(query: newText) page = 1 // 文本变化时重置页码,重新加载第一页 } } // 点击键盘上的搜索按钮时立即触发请求 .onSubmit(of: .search) { searchTask?.cancel() Task { await subListViewModel.searchMovies(query: searchText) page = 1 } } .tabItem { Image(systemName: "magnifyingglass") Text("搜索") } // 第二个标签页 Text("待开发") .tabItem { Image(systemName: "gear") Text("设置") } } } }
关键逻辑说明
- 防抖处理:用
Task延迟0.5秒执行请求,同时每次文本变化时取消之前的任务,避免输入过程中频繁调用API。 - 取消按钮行为:原生
searchable的取消按钮会自动清空searchText,触发onChange回调,进而调用ViewModel的方法清空搜索结果。 - 分页处理:监听列表滚动到底部,加载下一页;搜索文本变化时重置页码,重新加载第一页数据。
- 搜索建议:启用代码中注释的部分,就能展示搜索结果作为建议,点击建议会自动填充搜索框并触发请求。
内容的提问来源于stack exchange,提问作者Zhou Haibo
相关产品推荐
相关产品推荐

