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

Table View搜索功能失效问题求助

搜索栏无法过滤数据的问题修复

问题描述

已通过代码创建搜索栏并定义搜索功能,但在搜索栏输入文本时无法过滤数据,已在viewDidLoad方法中设置filteredData。

ViewModel 代码

enum MoviesViewModelState {
    case loading
    case loaded([Movie])
    case error

    var movies: [Movie] {
        switch self {
        case .loaded(let movies):
            return movies
        case .loading, .error:
            return []
        }
    }
}

final class MoviesViewModel {
    
    private let apiManager: APIManaging
    
    init(apiManager: APIManaging = APIManager()) {
        self.apiManager = apiManager
    }
    
    var updatedState: (() -> Void)?
    
    var state: MoviesViewModelState = .loading {
        didSet {
            updatedState?()
        }
    }
    
    func fetchData() {
        apiManager.execute(Movie.topRated) { [weak self] result in
            switch result {
            case .success(let page):
                self?.state = .loaded(page.results)
            case .failure:
                self?.state = .error
            }
        }
    }
}

ViewController 代码

import UIKit

final class MoviesViewController: UITableViewController, UISearchResultsUpdating {
    
    private let viewModel: MoviesViewModel
    private var filteredData: [Movie] = []
    var searchViewController: UISearchController!
    
    init(viewModel: MoviesViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        title = LocalizedString(key: "movies.title")

        NotificationCenter.default.addObserver(self, selector: #selector(textSizeChanged), name: UIContentSizeCategory.didChangeNotification, object: nil)
        filteredData = viewModel.state.movies
        searchViewController = UISearchController(searchResultsController: nil)
        searchViewController.searchResultsUpdater = self
        
        configureSearchBar()
        configureTableView()
        updateFromViewModel()
        bindViewModel()
        viewModel.fetchData()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        searchViewController.searchResultsUpdater = self
        navigationItem.searchController = searchViewController
        navigationItem.hidesSearchBarWhenScrolling = false
        definesPresentationContext = true
    }

    func updateSearchResults(for searchController: UISearchController) {
        let data = viewModel.state.movies
        if let searchText = searchController.searchBar.text {
            filteredData = searchText.isEmpty ? data : filteredData
        
            tableView.reloadData()
        }
    }

    private func configureTableView() {
        tableView.dm_registerClassWithDefaultIdentifier(cellClass: MovieCell.self)
        tableView.rowHeight = UITableView.automaticDimension

        refreshControl = UIRefreshControl()
        refreshControl?.addTarget(self, action: #selector(refreshData), for: .valueChanged)
    }

    private func bindViewModel() {
        viewModel.updatedState = { [weak self] in
            guard let self else { return }
            DispatchQueue.main.async {
                self.updateFromViewModel()
            }
        }
    }

    private func updateFromViewModel() {
        switch viewModel.state {
        case .loading, .loaded:
            tableView.reloadData()
        case .error:
            showError()
        }
        refreshControl?.endRefreshing()
    }

    private func showError() {
        let alertController = UIAlertController(title: "", message: LocalizedString(key: "movies.load.error.body"), preferredStyle: .alert)
        let alertAction = UIAlertAction(title: LocalizedString(key: "movies.load.error.actionButton"), style: .default, handler: nil)
        alertController.addAction(alertAction)
        present(alertController, animated: true, completion: nil)
    }

    @objc private func refreshData() {
        viewModel.fetchData()
    }

    @objc private func textSizeChanged() {
        tableView.reloadData()
    }

    private func configureSearchBar() {

        let searchTextField = searchViewController.searchBar.searchTextField
        searchTextField.attributedPlaceholder = NSAttributedString(string: "Search", attributes: [.font: UIFont.Body.medium, .foregroundColor: UIColor.Text.charcoal])
        searchTextField.font = UIFont(name: "Poppins-Regular", size: 16)
        searchTextField.backgroundColor = UIColor(red: 248 / 255.0, green: 248 / 255.0, blue: 248 / 255.0, alpha: 1)
        searchTextField.borderStyle = .none
        searchTextField.layer.borderColor = UIColor.black.withAlphaComponent(0.08).cgColor
        searchTextField.layer.borderWidth = 1.0
        searchTextField.layer.cornerRadius = 8

        searchViewController.searchBar.setLeftImage(UIImage(named: "Search"))
        searchViewController.searchBar.barTintColor = .clear
        searchViewController.searchBar.setImage(UIImage(named: "Filter"), for: .bookmark, state: .normal)
        searchViewController.searchBar.showsBookmarkButton = true
        searchViewController.searchBar.delegate = self
        searchViewController.searchBar.tintColor = UIColor.Brand.popsicle40
    }
}

// MARK: - UITableViewDataSource
extension MoviesViewController {

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return filteredData.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: MovieCell = tableView.dm_dequeueReusableCellWithDefaultIdentifier()

        let movie = filteredData[indexPath.row]
        cell.configure(movie)

        return cell
    }

}

// MARK: - UITableViewControllerDelegate
extension MoviesViewController {

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let movie = viewModel.state.movies[indexPath.row]
        let viewModel = MoviesDetailsViewModel(movie: movie, apiManager: APIManager())
        let viewController = MovieDetailsViewController(viewModel: viewModel)
        self.navigationController?.pushViewController(viewController, animated: true)
    }
}

extension MoviesViewController: UISearchBarDelegate {
    func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) {
        
    }
}

问题截图

搜索功能失效截图

问题原因及修复方案

核心问题分析

  1. 搜索逻辑缺失:updateSearchResults方法中,当搜索文本不为空时,filteredData直接赋值为自身,没有执行任何过滤操作,导致输入文本后数据无变化。
  2. 数据同步问题:viewDidLoad初始化时viewModel.state为.loading,filteredData被设为空数组;接口返回数据后,updateFromViewModel未更新filteredData,导致列表显示的初始数据为空或不同步。
  3. 跳转数据源不匹配:点击列表项时使用viewModel.state.movies获取数据,但列表展示的是filteredData,搜索过滤后会出现索引不匹配,导致跳转错误。

修复步骤

1. 完善搜索过滤逻辑

修改updateSearchResults方法,添加基于电影标题的过滤逻辑(可根据需求调整过滤字段):

func updateSearchResults(for searchController: UISearchController) {
    let data = viewModel.state.movies
    // 去除首尾空格,避免空字符干扰
    if let searchText = searchController.searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines), !searchText.isEmpty {
        filteredData = data.filter { movie in
            // 忽略大小写匹配标题
            movie.title.lowercased().contains(searchText.lowercased())
        }
    } else {
        filteredData = data
    }
    tableView.reloadData()
}

2. 同步接口返回后的filteredData

修改updateFromViewModel方法,确保数据加载完成后更新filteredData:

private func updateFromViewModel() {
    switch viewModel.state {
    case .loaded(let movies):
        filteredData = movies
        tableView.reloadData()
    case .loading:
        tableView.reloadData()
    case .error:
        showError()
    }
    refreshControl?.endRefreshing()
}

3. 修复跳转数据源不匹配问题

修改didSelectRowAt方法,使用filteredData获取当前点击的电影数据:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let movie = filteredData[indexPath.row]
    let viewModel = MoviesDetailsViewModel(movie: movie, apiManager: APIManager())
    let viewController = MovieDetailsViewController(viewModel: viewModel)
    navigationController?.pushViewController(viewController, animated: true)
}

4. 移除冗余设置

删除viewDidAppear中重复的searchViewController.searchResultsUpdater = self,该设置在viewDidLoad中已完成。

内容的提问来源于stack exchange,提问作者user21666271

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 20:34:57