SwiftUI自定义SearchBar绑定keyword未触发ViewModel调用fetchBooks展示列表如何解决
问题排查与修复方案
你代码的核心问题是没有关联搜索关键词变更与搜索请求触发逻辑,具体问题和修复方案如下:
核心问题
- 仅在
BookListViewModel初始化时调用了一次fetchBooks(),后续用户输入修改keyword后,没有逻辑触发重新请求数据,所以无法拿到新的搜索结果渲染列表 init()中self.keyword = keyword是无意义的自赋值,属于冗余代码- (可选优化)
BookViewModel不需要声明为ObservableObject类,你用到的所有属性都是初始化时确定的常量,改为结构体更符合SwiftUI设计规范,也没有多余的状态监听开销 - (调试优化)你当前在网络请求方法中用
catch直接返回空序列,请求或解析出错时没有日志输出,很难定位是接口问题还是数据解析问题
修复代码
1. 修改BookListViewModel逻辑,监听关键词变更触发搜索
import Combine class BookListViewModel: ObservableObject { @Published public private(set) var books: [BookViewModel] = [] // 用Set存储订阅,避免单次请求替换订阅导致的异常 private var cancellables = Set<AnyCancellable>() @Published var keyword : String = "" init() { // 监听keyword的变化 $keyword // 可选添加防抖:输入停顿300ms后再发起请求,避免频繁调用接口 .debounce(for: .milliseconds(300), scheduler: RunLoop.main) // 相同关键词不重复发起请求 .removeDuplicates() .sink { [weak self] newKeyword in guard let self = self else { return } // 关键词为空时直接清空列表,不发起无效请求 let trimedKeyword = newKeyword.trimmingCharacters(in: .whitespacesAndNewlines) if trimedKeyword.isEmpty { self.books = [] return } self.fetchBooks(keyword: trimedKeyword) } .store(in: &cancellables) } func fetchBooks(keyword: String) { Webservice().getSearchBooks(keyword: keyword) .map { books in books.map { BookViewModel(book: $0) } } .receive(on: RunLoop.main) .sink(receiveCompletion: { completion in if case .failure(let err) = completion { print("请求失败:\(err)") } }, receiveValue: { [weak self] bookViewModels in self?.books = bookViewModels }) .store(in: &cancellables) } } // 改为结构体,遵循Identifiable协议,列表渲染时不需要再手动指定id struct BookViewModel: Identifiable { let book: Book let id: String var title: String init(book: Book){ self.book = book self.id = book.isbn13 self.title = book.title } }
2. 优化网络请求方法,添加错误日志
func getSearchBooks(keyword: String) -> AnyPublisher<[Book], Error> { URLSession.shared.dataTaskPublisher(for: EndPoint.books(keyword).url) .map{ $0.data } .decode(type: BooksData.self, decoder: JSONDecoder()) .map{$0.books} .catch { error in // 添加错误打印,方便排查问题 print("网络请求/解析失败:\(error)") return Empty<[Book], Error>() } .receive(on: RunLoop.main) .eraseToAnyPublisher() }
3. 可选修改:ViewModel声明方式
如果你的BookListViewModel是在SearchView内部初始化的,建议将@ObservedObject改为@StateObject,避免视图重绘时ViewModel被反复销毁重建:
struct SearchView: View { // 内部初始化用@StateObject @StateObject var bookListVM = BookListViewModel() // 如果是父视图传入则保留@ObservedObject即可 // @ObservedObject var bookListVM : BookListViewModel // ... 其余代码保持不变 }
内容的提问来源于stack exchange,提问作者J L
相关产品推荐
相关产品推荐

