You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 22:44:56