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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.07 06:24:00